package de.avatic.lcc.service.calculation.execution;

import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.model.db.error.SysError;
import de.avatic.lcc.model.db.error.SysErrorType;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
import de.avatic.lcc.repositories.error.SysErrorRepository;
import de.avatic.lcc.service.transformer.error.SysErrorTransformer;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Optional;

@Component
public class CalculationJobProcessor {

    private static final Logger log = LoggerFactory.getLogger(CalculationJobProcessor.class);

    private final CalculationJobRepository calculationJobRepository;
    private final SysErrorTransformer sysErrorTransformer;
    private final SysErrorRepository sysErrorRepository;
    private final CalculationExecutionService calculationExecutionService;

    @Value("${calculation.job.processor.enabled:true}")
    private boolean enabled;

    private volatile  boolean shutdownRequested = false;

    public CalculationJobProcessor(CalculationJobRepository calculationJobRepository, SysErrorTransformer sysErrorTransformer, SysErrorRepository sysErrorRepository, CalculationExecutionService calculationExecutionService) {
        this.calculationJobRepository = calculationJobRepository;
        this.sysErrorTransformer = sysErrorTransformer;
        this.sysErrorRepository = sysErrorRepository;
        this.calculationExecutionService = calculationExecutionService;
    }

    private void processNextJob(CalculationJob job) {

        log.info("Processing calculation job {} with priority {} (retries: {})",
                job.getId(), job.getPriority(), job.getRetries());

        try {

            executeCalculation(job);

            calculationJobRepository.markAsValid(job.getId());
            log.info("Successfully completed calculation job {}", job.getId());

        } catch (Exception e) {
            // Job fehlgeschlagen
            log.error("Error processing calculation job {}: {}", job.getId(), e.getMessage() == null ? "n/a" : e.getMessage(), e);

            Integer errorId = buildError(e, job);

            calculationJobRepository.markAsException(job.getId(), errorId);

            if (job.getRetries() >= 3) {
                log.warn("Calculation job {} failed after {} retries", job.getId(), job.getRetries());
            } else {
                log.info("Calculation job {} marked for retry (attempt {}/3)", job.getId(), job.getRetries());
            }
        }
    }

    @PreDestroy
    public void onShutdown() {
        log.info("Shutdown requested, stopping job processing after current jobs...");
        shutdownRequested = true;
    }

    @Scheduled(fixedDelayString = "${calculation.job.processor.delay:5000}")
    public void entryPoint() {
        if (!enabled) {
            return;
        }

        try {
            while (!shutdownRequested && !Thread.currentThread().isInterrupted()) {

                Optional<CalculationJob> jobOpt = calculationJobRepository.fetchAndLockNextJob();

                if (jobOpt.isEmpty()) {
                    // no jobs -> go to sleep
                    return;
                }

                CalculationJob job = jobOpt.get();

                processNextJob(job);

            }


        } catch (Exception e) {
            log.error("Unexpected error in job processor: {}", e.getMessage(), e);
            buildError(e, null);
        }
    }

    private Integer buildError(Exception e, CalculationJob job) {
        var error = new SysError();

        error.setType(job == null ? SysErrorType.BACKEND : SysErrorType.CALCULATION);

        error.setCode(e.getClass().getSimpleName().substring(0, Math.min(e.getClass().getSimpleName().length(), 253)));
        error.setTitle(job == null ? "Unexpected error in job processor" : String.format("Calculation operation execution %s failed", job.getId()));

        var msg = e.getMessage() == null ? "" : e.getMessage();

        error.setMessage(msg.substring(0, Math.min(msg.length(), 1000)));
        error.setUserId(job == null ? null : job.getUserId());
        error.setCalculationJobId(job == null ? null : job.getId());
        error.setTrace(Arrays.stream(e.getStackTrace()).map(sysErrorTransformer::toSysErrorTraceItem).toList());

        return sysErrorRepository.insert(error);
    }


    private void executeCalculation(CalculationJob job) throws Exception {

        log.info("Executing calculation for job {} (premise: {}, period: {}, set: {})",
                job.getId(), job.getPremiseId(), job.getValidityPeriodId(), job.getPropertySetId());

        var startTime = System.currentTimeMillis();
        calculationExecutionService.launchJobCalculation(job);
        var stopTime = System.currentTimeMillis();

        log.info("Executing calculation for job {} finished (execution time: {}ms)",
                job.getId(), stopTime - startTime);
    }
}