package com.devjob.service;

import com.devjob.domain.Company;
import com.devjob.domain.Job;
import com.devjob.domain.JobStatus;
import com.devjob.dto.JobCreateRequest;
import com.devjob.dto.JobSearchRequest;
import com.devjob.dto.JobUpdateRequest;
import com.devjob.dto.JobResponse;
import com.devjob.dto.PageResponse;
import com.devjob.exception.BusinessException;
import com.devjob.exception.ErrorCode;
import com.devjob.repository.CompanyRepository;
import com.devjob.repository.JobRepository;
import com.devjob.util.SortValidator;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * EP13: Job ↔ Company 연관관계 적용.
 * 등록 시 companyId로 Company를 조회해 연관관계를 맺고,
 * 목록 조회는 fetch join 쿼리(findByStatusWithCompany)로 N+1을 방지한다.
 */
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class JobService {

    /**
     * EP29: 정렬 허용 필드 화이트리스트. company.name 같은 연관 엔티티 필드는
     * Specification 쿼리에서 정렬 시 추가 join이 필요해 의도적으로 제외했다.
     */
    private static final Set<String> ALLOWED_SORT_PROPERTIES =
            Set.of("id", "title", "salary", "status", "deadline", "createdAt", "updatedAt");

    private final JobRepository jobRepository;
    private final CompanyRepository companyRepository;

    /**
     * EP34: 공고 등록/수정/삭제 시 "jobs" 캐시를 전체 무효화해
     * 캐시된 목록과 DB 데이터가 어긋나지 않도록 한다.
     */
    @CacheEvict(value = "jobs", allEntries = true)
    @Transactional
    public JobResponse create(JobCreateRequest req, Long registeredBy) {
        Company company = companyRepository.findById(req.companyId())
                .orElseThrow(() -> new BusinessException(ErrorCode.COMPANY_NOT_FOUND));

        Job job = new Job();
        job.setCompany(company);
        job.setTitle(req.title());
        job.setDescription(req.description());
        job.setSalary(req.salary());
        job.setStatus(JobStatus.OPEN);
        job.setDeadline(req.deadline());
        job.setRegisteredBy(registeredBy);   // 작성자 = 로그인 회원
        return JobResponse.from(jobRepository.save(job));
    }

    public Optional<JobResponse> findById(Long id) {
        return jobRepository.findById(id)
                .map(JobResponse::from);
    }

    /**
     * EP34: 목록 조회는 자주 호출되지만 자주 바뀌지 않으므로 60초간 Redis에 캐싱한다(CacheConfig).
     * 캐시 키는 status+page+size+sort 조합으로 구성해, 조건이 다르면 다른 캐시 항목을 사용한다.
     */
    @Cacheable(value = "jobs", key = "#status + '-' + #pageable.pageNumber + '-' + #pageable.pageSize + '-' + #pageable.sort")
    public PageResponse<JobResponse> findByStatus(JobStatus status, Pageable pageable) {
        SortValidator.validate(pageable.getSort(), ALLOWED_SORT_PROPERTIES);
        return PageResponse.of(jobRepository.findByStatusWithCompany(status, pageable).map(JobResponse::from));
    }

    /**
     * EP14: 제목/회사명/지역 통합 검색. JPQL @Query(searchJobs)로 OR + JOIN FETCH 조건을 처리한다.
     * EP27부터 컨트롤러는 Specification 기반 search(JobSearchRequest, Pageable)를 사용하지만,
     * searchJobs 자체는 JPQL @Query 학습 예시로 레포지토리에 남겨둔다.
     */
    public PageResponse<JobResponse> search(String keyword, String location, Pageable pageable) {
        return PageResponse.of(jobRepository.searchJobs(keyword, location, pageable).map(JobResponse::from));
    }

    /**
     * EP27: status/연봉 범위까지 포함한 동적 검색. JpaSpecificationExecutor.findAll(spec, pageable) 사용.
     *
     * 주의(N+1): Specification 기반 findAll은 fetch join을 적용하기 까다로워
     * company는 LAZY로 각 행마다 추가 조회될 수 있다. 이 트레이드오프는 Part5(성능 개선)에서
     * @EntityGraph 등으로 다룬다.
     */
    public PageResponse<JobResponse> search(JobSearchRequest request, Pageable pageable) {
        SortValidator.validate(pageable.getSort(), ALLOWED_SORT_PROPERTIES);
        return PageResponse.of(jobRepository.findAll(request.toSpecification(), pageable).map(JobResponse::from));
    }

    /**
     * EP16: PUT /api/jobs/{id} — 영속 엔티티를 변경 감지(dirty checking)로 수정한다.
     */
    @CacheEvict(value = "jobs", allEntries = true)
    @Transactional
    public JobResponse update(Long id, JobUpdateRequest req, Long requesterId, boolean isAdmin) {
        Job job = jobRepository.findById(id)
                .orElseThrow(() -> new BusinessException(ErrorCode.JOB_NOT_FOUND));
        checkOwner(job, requesterId, isAdmin);
        job.update(req.title(), req.description(), req.salary(), req.deadline());
        return JobResponse.from(job);
    }

    @CacheEvict(value = "jobs", allEntries = true)
    @Transactional
    public void delete(Long id) {
        if (!jobRepository.existsById(id)) {
            throw new BusinessException(ErrorCode.JOB_NOT_FOUND);
        }
        jobRepository.deleteById(id);
    }

    @Transactional
    public void delete(Long id, Long requesterId, boolean isAdmin) {
        Job job = jobRepository.findById(id)
                .orElseThrow(() -> new BusinessException(ErrorCode.JOB_NOT_FOUND));
        checkOwner(job, requesterId, isAdmin);
        jobRepository.deleteById(id);
    }

    private void checkOwner(Job job, Long requesterId, boolean isAdmin) {
        if (!isAdmin && !job.getRegisteredBy().equals(requesterId)) {
            throw new BusinessException(ErrorCode.FORBIDDEN);
        }
    }
}
