package com.oguerreiro.resilient.activities.period.process;

import java.time.Instant;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.activities.period.ClearResilientLogTask;
import com.oguerreiro.resilient.activity.AbstractActivity;
import com.oguerreiro.resilient.activity.GenericGuide;
import com.oguerreiro.resilient.activity.GenericResult;
import com.oguerreiro.resilient.activity.TaskResult;
import com.oguerreiro.resilient.activity.WorkResultEnum;
import com.oguerreiro.resilient.activity.registry.ActivityProgressDescriptor;
import com.oguerreiro.resilient.activity.registry.ActivityProgressRegistry;
import com.oguerreiro.resilient.domain.Period;
import com.oguerreiro.resilient.domain.PeriodVersion;
import com.oguerreiro.resilient.domain.enumeration.PeriodStatus;
import com.oguerreiro.resilient.domain.enumeration.PeriodVersionStatus;
import com.oguerreiro.resilient.repository.PeriodRepository;
import com.oguerreiro.resilient.repository.PeriodVersionRepository;
import com.oguerreiro.resilient.security.SecurityUtils;
import com.oguerreiro.resilient.service.PeriodService;

import jakarta.persistence.EntityManager;

@Component
public class PeriodProcessOutputsActivityImpl
    extends AbstractActivity<GenericResult, GenericGuide<Period>, PeriodProcessOutputsActivity>
    implements PeriodProcessOutputsActivity {

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

  private static final String ACTIVITY_KEY = "period-process-outputs";

  /* ----- TASKS ----- */
  private final ClearResilientLogTask clearResilientLogTask;
  private final ClearOutputDataTask clearOutputDataTask;
  private final GenerateOutputDataTask generateOutputDataTask;

  /* ----- BEANS ----- */
  private final PeriodRepository periodRepository;
  private final PeriodService periodService;
  private final ActivityProgressRegistry activityProgressRegistry;
  private final EntityManager entityManager;

  /* ----- Constructor ----- */
  public PeriodProcessOutputsActivityImpl(PeriodRepository periodRepository,
      ActivityProgressRegistry activityProgressRegistry, PeriodVersionRepository periodVersionRepository,
      EntityManager entityManager, ClearOutputDataTask clearOutputDataTask,
      GenerateOutputDataTask generateOutputDataTask, ClearResilientLogTask clearResilientLogTask,
      PeriodService periodService) {
    this.periodRepository = periodRepository;
    this.activityProgressRegistry = activityProgressRegistry;
    this.periodService = periodService;
    this.entityManager = entityManager;
    this.clearOutputDataTask = clearOutputDataTask;
    this.generateOutputDataTask = generateOutputDataTask;
    this.clearResilientLogTask = clearResilientLogTask;
  }

  /* ----- SelectionStrategy properties ----- */
  @Override
  public Class appliesToActivityDomainClass() {
    return Period.class;
  }

  @Override
  public List<String> appliesToStates() {
    return Arrays.asList(PeriodStatus.CLOSED.toString(), PeriodStatus.PROCESSING.toString());
  }

  /* ----- Activity implementation ----- */
  @Override
  public String getActivityKey() {
    return ACTIVITY_KEY;
  }

  @Transactional
  @Override
  public GenericResult doActivity(String activityDomainClass, Long activityDomainId) {
    // Load Period
    Period period = periodRepository.getReferenceById(activityDomainId);

    // Validations
    if (!period.getState().equals(PeriodStatus.CLOSED) && !period.getState().equals(PeriodStatus.PROCESSING)) {
      throw new RuntimeException("Activity Process Outputs, applies only to CLOSED period state.");
    }

    if (period.getState().equals(PeriodStatus.PROCESSING)) {
      // Must have a PeriodVersion in state ERROR or PROCESSING (if something critical
      // happened to the server before)
      // TODO
    } else {
      // In this case, can't have pending PeriodVersions. Search for any 'non-ended'
      // PeriodVersion
      boolean hasActivePeriodVersions = period.getPeriodVersions().stream().anyMatch(
          periodVersion -> !periodVersion.getState().equals(PeriodVersionStatus.ENDED));
      if (hasActivePeriodVersions) {
        throw new RuntimeException("The period have PeriodVersion's not ENDED. Process the PeriodVersion instead.");
      }
    }

    PeriodVersion periodVersion = null;

    if (period.getState().equals(PeriodStatus.CLOSED)) {
      // Create a PeriodVersion, only when the Period is CLOSE (not reprocessing)

      // Get the max PeriodVersion
      int periodVersionIndex = 1;
      PeriodVersion maxPeriodVersion = period.getPeriodVersions().stream().max(
          Comparator.comparingInt(PeriodVersion::getPeriodVersion)).orElse(null);
      if (maxPeriodVersion != null) {
        periodVersionIndex = maxPeriodVersion.getPeriodVersion() + 1;
      }

      // Create PeriodVersion to (really) process the outputs
      periodVersion = new PeriodVersion();
      periodVersion.name(period.getName() + " - " + periodVersionIndex).creationDate(Instant.now()).creationUsername(
          SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found"))).description(
              period.getName() + " - " + periodVersionIndex).period(period).periodVersion(periodVersionIndex).state(
                  PeriodVersionStatus.PROCESSING);
    }

    // Save & Change status
    period = self().updateStateWithNewTransaction(period, PeriodStatus.PROCESSING, periodVersion);

    // Fire PeriodVersion activity
    ActivityProgressDescriptor activityProgressDescriptor = activityProgressRegistry.progressAdd(period);
    GenericGuide<Period> guide = GenericGuide.create(activityProgressDescriptor, period,
        SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found")));
    self().doInternalActivityAsync(guide);

    // Return without waiting
    return new GenericResult(WorkResultEnum.COMPLETED);
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public Period updateStateWithNewTransaction(Period period, PeriodStatus periodStatus,
      PeriodVersion newPeriodVersion) {
    period = periodRepository.getReferenceById(period.getId());
    period.setState(periodStatus);

    if (newPeriodVersion != null) {
      period.addPeriodVersion(newPeriodVersion);
    }

    return periodRepository.save(period);
  }

  @Async
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  @Override
  public CompletableFuture<GenericResult> doInternalActivityAsync(GenericGuide<Period> guide) {
    log.debug("Request to doInternalActivityAsync : {}", guide);

    GenericResult genericResult = null;
    try {
      genericResult = this.doInternalActivity(guide);

      guide.getActivityProgressDescriptor().updateProgress(100, "Finished");

      //Update Period & PeriodVersion state
      Period period = guide.getActivityDomain();
      PeriodVersion periodVersion = (PeriodVersion) guide.otherValues.get(GUIDE_OTHERVALUE_PERIODVERSION);

      period.setState(PeriodStatus.ENDED);
      periodVersion.setState(PeriodVersionStatus.ENDED);
      periodRepository.save(period);

    } catch (Throwable t) {
      log.error("Error in doInternalActivityAsync : {}", t);
      Period period = guide.getActivityDomain();

      this.logError(period, guide.getUsername(), t);

      // Stop progress
      guide.getActivityProgressDescriptor().updateProgress(100, "Error");

      // Update PeriodVersion state to ERROR
      PeriodVersion periodVersion = (PeriodVersion) guide.otherValues.get(GUIDE_OTHERVALUE_PERIODVERSION);
      periodVersion.setState(PeriodVersionStatus.ERROR);
      periodService.saveWithNewTransaction(period); // The current transaction will be rollbacked, thats why it has to be done in a new transaction

      throw t;
    }

    return CompletableFuture.completedFuture(genericResult);
  }

  private GenericResult doInternalActivity(GenericGuide<Period> guide) {
    log.debug("Request to doInternalActivity : {}", guide);

    Period period = guide.getActivityDomain();

    // Re-attach period
    guide.setActivityDomain(entityManager.merge(period));
    period = guide.getActivityDomain();
    log.debug("Request to doInternalActivity - merged period : {}", guide);

    PeriodVersion periodVersion = null;
    // Get the processing PeriodVersion. One of PROCESSING or ERROR. But only one
    List<PeriodVersion> periodVersions = period.getPeriodVersions().stream().filter(
        pv -> !pv.getState().equals(PeriodVersionStatus.ENDED)).collect(Collectors.toList());

    if (periodVersions.size() != 1) {
      throw new RuntimeException(
          "More than one PeriodVersion was found, ready for PROCESSING. [" + periodVersions.size() + "]");
    }

    periodVersion = periodVersions.get(0);

    guide.otherValues.put(PeriodProcessOutputsActivity.GUIDE_OTHERVALUE_PERIODVERSION, periodVersion);

    TaskResult tr;

    // Task 0 - Clear logs
    guide.getActivityProgressDescriptor().updateProgress(5, "Clear logs");
    tr = this.clearResilientLogTask.doTask(guide);
    if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) {
      // STOP activity execution
      return new GenericResult(WorkResultEnum.FAILED);
    }

    // Task 1 - Clear any OutputData currently generated for the Period and
    // PeriodVersion
    // Probably a re-processing
    guide.getActivityProgressDescriptor().updateProgress(15, "Clear previous outputs");
    tr = this.clearOutputDataTask.doTask(guide);
    if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) {
      // STOP activity execution
      this.logError(periodVersion, guide.getUsername(), "Clear previous outputs - FAILED");
      return new GenericResult(WorkResultEnum.FAILED);
    }
    this.logInfo(periodVersion, guide.getUsername(), "Cleared previous outputs");

    // Task 2 - Generate outputs
    guide.getActivityProgressDescriptor().updateProgress(30, "Calculate Outputs");
    tr = this.generateOutputDataTask.doTask(guide);
    if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) {
      // STOP activity execution
      this.logError(periodVersion, guide.getUsername(), "Calculate Outputs - FAILED");
      return new GenericResult(WorkResultEnum.FAILED);
    }
    this.logInfo(periodVersion, guide.getUsername(), "Calculated Outputs");

    // If the progress got this far, no errors were throwned
    // Update progress
    guide.getActivityProgressDescriptor().updateProgress(90, "Update state");
    GenericResult result = null;

    //Create result response for activity
    result = new GenericResult(WorkResultEnum.COMPLETED);

    return result;
  }
}
