package com.oguerreiro.resilient.activities.inputDataImporter;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;

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.inputDataImporter.importers.InputDataImporterTask;
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.InputDataUpload;
import com.oguerreiro.resilient.domain.Period;
import com.oguerreiro.resilient.domain.enumeration.PeriodStatus;
import com.oguerreiro.resilient.domain.enumeration.UploadStatus;
import com.oguerreiro.resilient.domain.enumeration.UploadType;
import com.oguerreiro.resilient.error.ImporterPeriodNotOpenedToReceiveDataRuntimeException;
import com.oguerreiro.resilient.repository.InputDataUploadLogRepository;
import com.oguerreiro.resilient.repository.InputDataUploadRepository;
import com.oguerreiro.resilient.repository.PeriodRepository;
import com.oguerreiro.resilient.security.SecurityUtils;

import jakarta.persistence.EntityManager;

@Component
public class POIFileImporterActivityImpl
    extends AbstractActivity<GenericResult, GenericGuide<InputDataUpload>, POIFileImporterActivity>
    implements POIFileImporterActivity {
  private final Logger log = LoggerFactory.getLogger(POIFileImporterActivityImpl.class);

  private static final String ACTIVITY_KEY = "input-data-upload-import";

  /* ----- TASKS ----- */
  private final ClearInputDataUploadLogTask clearInputDataUploadLogTask;
  private final ClearInputDataTask clearInputDataTask;
  private final List<InputDataImporterTask> inputDataImporterTasks;

  /* ----- BEANS ----- */
  private final PeriodRepository periodRepository;
  private final InputDataUploadLogRepository inputDataUploadLogRepository;
  private final InputDataUploadRepository inputDataUploadRepository;
  private final EntityManager entityManager;
  private final ActivityProgressRegistry activityProgressRegistry;

  public POIFileImporterActivityImpl(ClearInputDataUploadLogTask clearInputDataUploadLogTask,
      ClearInputDataTask clearInputDataTask, InputDataUploadLogRepository inputDataUploadLogRepository,
      InputDataUploadRepository inputDataUploadRepository, EntityManager entityManager,
      ActivityProgressRegistry activityProgressRegistry, List<InputDataImporterTask> inputDataImporterTasks,
      PeriodRepository periodRepository) {
    this.clearInputDataUploadLogTask = clearInputDataUploadLogTask;
    this.clearInputDataTask = clearInputDataTask;
    this.inputDataUploadLogRepository = inputDataUploadLogRepository;
    this.inputDataUploadRepository = inputDataUploadRepository;
    this.entityManager = entityManager;
    this.activityProgressRegistry = activityProgressRegistry;
    this.inputDataImporterTasks = inputDataImporterTasks;
    this.periodRepository = periodRepository;
  }

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

  @Override
  public List<String> appliesToStates() {
    return Arrays.asList(UploadStatus.UPLOADED.toString(), UploadStatus.ERROR.toString());
  }

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

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

    ActivityProgressDescriptor activityProgressDescriptor = activityProgressRegistry.progressAdd(inputDataUpload);

    GenericGuide<InputDataUpload> guide = GenericGuide.create(activityProgressDescriptor, inputDataUpload,
        SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new RuntimeException("Value not found")));

    // Validate the state
    if (!inputDataUpload.getState().equals(UploadStatus.UPLOADED)
        && !inputDataUpload.getState().equals(UploadStatus.ERROR)) {
      // Error. Only files in UPLOADED state can process the LOAD operation
      throw new RuntimeException("Only files in UPLOADED state can process the LOAD operation");
    }

    // Validate the Period (it's OPEN ?)
    Period period = this.periodRepository.getReferenceById(inputDataUpload.getPeriod().getId());
    if (!period.getState().equals(PeriodStatus.OPENED)) {
      throw new ImporterPeriodNotOpenedToReceiveDataRuntimeException(period.getName(), period.getState());
    }

    // Update state
    inputDataUpload = self().updateStateWithNewTransaction(inputDataUpload, UploadStatus.PROCESSING);
    guide.setActivityDomain(inputDataUpload);

    // Fire async activity
    self().doInternalActivityAsync(guide);

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

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public InputDataUpload updateStateWithNewTransaction(InputDataUpload inputDataUpload, UploadStatus newUploadStatus) {
    inputDataUpload = inputDataUploadRepository.getReferenceById(inputDataUpload.getId());
    inputDataUpload.setState(newUploadStatus);
    inputDataUpload = inputDataUploadRepository.save(inputDataUpload);
    return inputDataUploadRepository.save(inputDataUpload);
  }

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

    GenericResult genericResult = null;
    try {
      genericResult = this.doInternalActivity(guide);
    } catch (Throwable t) {
      log.error("Error in doInternalActivityAsync : {}", t);
      throw t;
    } finally {
      InputDataUpload inputDataUpload = guide.getActivityDomain();

      //Always complete the activity and progress
      if (genericResult == null || !genericResult.getWorkResult().equals(WorkResultEnum.COMPLETED)) {
        inputDataUpload.setState(UploadStatus.ERROR);
        guide.getActivityProgressDescriptor().updateProgress(100, "Error");
      } else {
        inputDataUpload.setState(UploadStatus.PROCESSED);
        guide.getActivityProgressDescriptor().updateProgress(100, "Completed");
      }

      this.inputDataUploadRepository.updateInNewTransaction(inputDataUpload);
    }

    return CompletableFuture.completedFuture(genericResult);
  }

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

    InputDataUpload inputDataUpload = guide.getActivityDomain();

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

    TaskResult tr;

    // Task 1 - Clear logs and old data
    guide.getActivityProgressDescriptor().updateProgress(15, "Cleanning logs");
    tr = this.clearInputDataUploadLogTask.doTask(guide);
    if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) {
      // STOP activity execution
      return new GenericResult(WorkResultEnum.FAILED);
    }

    // Task 2 - Clear old InputData, previously imported
    guide.getActivityProgressDescriptor().updateProgress(30, "Cleanning previous imported data");
    tr = this.clearInputDataTask.doTask(guide);
    if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) {
      // STOP activity execution
      return new GenericResult(WorkResultEnum.FAILED);
    }

    // Task 3 - Import data from Excel file
    guide.getActivityProgressDescriptor().updateProgress(45, "Importing data.");
    InputDataImporterTask task = this.findImporterTask(inputDataUpload.getType());
    tr = task.doTask(guide);
    if (!WorkResultEnum.COMPLETED.equals(tr.getWorkResult())) {
      // STOP activity execution
      return new GenericResult(WorkResultEnum.FAILED);
    }

    // Count erros in InputDataUploadLog's
    guide.getActivityProgressDescriptor().updateProgress(90, "Evaluating errors");
    GenericResult result = null;
    Integer errorCount = self().countErrors(inputDataUpload);
    if (errorCount > 0) {
      inputDataUpload.setState(UploadStatus.ERROR);
      result = new GenericResult(WorkResultEnum.FAILED);
    } else {
      inputDataUpload.setState(UploadStatus.PROCESSED);
      result = new GenericResult(WorkResultEnum.COMPLETED);
    }

    guide.getActivityProgressDescriptor().updateProgress(95, "Update state");
    inputDataUploadRepository.updateInNewTransaction(inputDataUpload);
    guide.getActivityProgressDescriptor().updateProgress(100, "Finished");
    return result;
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public Integer countErrors(InputDataUpload inputDataUpload) {
    Integer errorCount = inputDataUploadLogRepository.countByInputDataUpload(inputDataUpload);
    return errorCount;
  }

  private InputDataImporterTask findImporterTask(UploadType uploadType) {
    for (InputDataImporterTask inputDataImporterTask : inputDataImporterTasks) {
      if (inputDataImporterTask.getUploadType().equals(uploadType)) {
        return inputDataImporterTask;
      }
    }

    return null;
  }
}
