package ai.decompile.job.service;

import ai.decompile.common.exception.ConflictException;
import ai.decompile.common.exception.NotFoundException;
import ai.decompile.job.messaging.JobMessagePublisher;
import ai.decompile.job.model.dto.JobListResponse;
import ai.decompile.job.model.dto.JobResponse;
import ai.decompile.job.model.entity.Job;
import ai.decompile.job.model.enums.JobStatus;
import ai.decompile.job.model.enums.JobType;
import ai.decompile.job.model.repository.JobRepository;
import ai.decompile.job.model.specification.JobSpecification;
import ai.decompile.workspace.event.BinaryUploadedEvent;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Log4j2
@Service
@RequiredArgsConstructor
public class JobService {
  private final JobRepository jobRepository;
  private final JobMessagePublisher jobMessagePublisher;

  public JobListResponse getJobs(UUID projectId, JobStatus status, JobType type, String search) {
    var jobs =
        jobRepository
            .findAll(JobSpecification.withFilters(projectId, status, type, search))
            .stream()
            .map(JobResponse::from)
            .toList();

    return new JobListResponse(
        jobs,
        jobRepository.count(JobSpecification.hasStatus(JobStatus.STARTED))
            + jobRepository.count(JobSpecification.hasStatus(JobStatus.IN_PROGRESS)),
        jobRepository.count(JobSpecification.hasStatus(JobStatus.ENQUEUED)),
        jobRepository.count(JobSpecification.hasStatus(JobStatus.COMPLETED)),
        jobRepository.count(JobSpecification.hasStatus(JobStatus.FAILED)));
  }

  public JobResponse getJob(UUID jobId) {
    var job = findJob(jobId);
    return JobResponse.from(job);
  }

  @Transactional
  public JobResponse cancelJob(UUID jobId) {
    var job = findJob(jobId);

    if (job.getStatus() == JobStatus.COMPLETED
        || job.getStatus() == JobStatus.FAILED
        || job.getStatus() == JobStatus.CANCELED) {
      throw new ConflictException(
          "Job with id '"
              + jobId
              + "' cannot be canceled because it is already "
              + job.getStatus());
    }

    job.setStatus(JobStatus.CANCELED);
    return JobResponse.from(jobRepository.save(job));
  }

  @ApplicationModuleListener
  void onBinaryUploaded(BinaryUploadedEvent event) {
    log.info(
        "Received BinaryUploadedEvent for binary={} project={}",
        event.binaryId(),
        event.projectId());
    var job = createJob(event);
    jobMessagePublisher.publish(job.getId());
    log.info("Published ANALYZE_FILE job {} to RabbitMQ", job.getId());
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  Job createJob(BinaryUploadedEvent event) {
    return jobRepository.save(
        Job.builder()
            .type(JobType.ANALYZE_FILE)
            .status(JobStatus.ENQUEUED)
            .projectId(event.projectId())
            .binaryId(event.binaryId())
            .retryCount(0)
            .build());
  }

  private Job findJob(UUID jobId) {
    return jobRepository
        .findById(jobId)
        .orElseThrow(() -> new NotFoundException("Job with id '" + jobId + "' not found"));
  }
}
