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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.activity.AbstractTask;
import com.oguerreiro.resilient.activity.GenericGuide;
import com.oguerreiro.resilient.activity.GenericResult;
import com.oguerreiro.resilient.activity.WorkResultEnum;
import com.oguerreiro.resilient.domain.Organization;
import com.oguerreiro.resilient.domain.OutputData;
import com.oguerreiro.resilient.domain.Period;
import com.oguerreiro.resilient.domain.PeriodVersion;
import com.oguerreiro.resilient.domain.Variable;
import com.oguerreiro.resilient.error.ExpressionEvaluationException;
import com.oguerreiro.resilient.error.OutputExpressionEvaluationRuntimeException;
import com.oguerreiro.resilient.error.OutputExpressionNullResultRuntimeException;
import com.oguerreiro.resilient.error.PeriodProcessDependenciesNotSatisfyedException;
import com.oguerreiro.resilient.expression.ExpressionUtils;
import com.oguerreiro.resilient.expression.FunctionCall;
import com.oguerreiro.resilient.expression.OutputVariableExpressionContext;
import com.oguerreiro.resilient.expression.OutputVariableExpressionFunctions;
import com.oguerreiro.resilient.repository.EmissionFactorRepository;
import com.oguerreiro.resilient.repository.InputDataRepository;
import com.oguerreiro.resilient.repository.OrganizationRepository;
import com.oguerreiro.resilient.repository.OutputDataRepository;
import com.oguerreiro.resilient.repository.VariableRepository;

@Qualifier("myGenerateOutputDataTask")
@Component
public class GenerateOutputDataTaskImpl extends
    AbstractTask<GenericResult, GenericGuide<Period>, GenerateOutputDataTask> implements GenerateOutputDataTask {

  private final OutputDataRepository outputDataRepository;
  private final VariableRepository variableRepository;
  private final OrganizationRepository organizationRepository;
  private final EmissionFactorRepository emissionFactorRepository;
  private final InputDataRepository inputDataRepository;
  private final ApplicationContext applicationContext;

  public GenerateOutputDataTaskImpl(VariableRepository variableRepository,
      OrganizationRepository organizationRepository, EmissionFactorRepository emissionFactorRepository,
      InputDataRepository inputDataRepository, ApplicationContext applicationContext,
      OutputDataRepository outputDataRepository) {
    this.variableRepository = variableRepository;
    this.organizationRepository = organizationRepository;
    this.emissionFactorRepository = emissionFactorRepository;
    this.inputDataRepository = inputDataRepository;
    this.applicationContext = applicationContext;
    this.outputDataRepository = outputDataRepository;
  }

  @Override
  public GenericResult doTask(GenericGuide<Period> guide) {
    this.calculateGlobalOutputs(guide);
    this.calculateSingleOutputs(guide);

    return new GenericResult(WorkResultEnum.COMPLETED);
  }

  private void calculateGlobalOutputs(GenericGuide<Period> guide) {
    Integer progressTotalTaskPercentage = 70;

    // Get all output NOT single variables
    List<Variable> outputVariables = variableRepository.findAllOutputNotSingle();

    String progressTaskText = guide.getActivityProgressDescriptor().getProgressTask();
    Integer progressPercentage = guide.getActivityProgressDescriptor().getProgressPercentage();

    // Create an calculation plan. This ensures the correct order of calculation by dependencies
    guide.getActivityProgressDescriptor().updateProgress(progressPercentage,
        progressTaskText + " - Create calculation plan");
    outputVariables = calculatePlan(outputVariables);

    // Get a list of all Organizations with output generation
    List<Organization> outputOrgs = organizationRepository.findAllOrganizationForOutput();
    Integer countOrgs = outputOrgs.size();
    Integer progressOrgPercentage = progressTotalTaskPercentage / countOrgs;
    guide.otherValues.put("PROGRESS_ORG", progressOrgPercentage);
    Integer processedOrgs = 0;

    for (Organization organization : outputOrgs) {
      // Refresh progress & update task text
      progressOrgPercentage = processedOrgs * progressTotalTaskPercentage / countOrgs;
      guide.getActivityProgressDescriptor().updateProgress(progressPercentage + progressOrgPercentage,
          progressTaskText);

      // Calculate Org
      this.logInfo(guide.getActivityDomain(), guide.getUsername(),
          "Started Output Calculation for " + organization.getCode());
      self().calculateOrganization(guide, organization, outputVariables);
      this.logInfo(guide.getActivityDomain(), guide.getUsername(),
          "Ended Output Calculation for " + organization.getCode());

      processedOrgs++;
    }
  }

  private void calculateSingleOutputs(GenericGuide<Period> guide) {
    // Get all output single variables
    List<Variable> outputVariables = variableRepository.findAllOutputSingle();

    for (Variable variable : outputVariables) {
      self().calculateOrganization(guide, variable.getOutputOwner(), List.of(variable));
    }
  }

  private List<Variable> calculatePlan(List<Variable> outputVariables)
      throws PeriodProcessDependenciesNotSatisfyedException {
    List<Variable> sortedOutputVariables = new ArrayList<Variable>();
    List<String> sortedOutputVariableCodes = new ArrayList<String>(); //For debug purpose only
    Map<Variable, Set<String>> variableDependencies = new HashMap<Variable, Set<String>>();
    Map<Variable, Set<String>> variableWildcardDependencies = new HashMap<Variable, Set<String>>();
    Set<String> dependencies;
    boolean hasWildcard = false;

    // Create a list of variables and its dependencies
    for (Variable variable : outputVariables) {
      //Initialize
      dependencies = new HashSet<String>();
      hasWildcard = false;

      //Calculate dependencies list
      List<FunctionCall> functionCalls = extractFunctionCalls(variable.getOutputFormula(), "out");

      for (FunctionCall fCall : functionCalls) {
        String code = extractVariableCodeFromOutFunction(fCall.getArguments());
        if (code != null) {
          dependencies.add(code);
          if (code.contains("*"))
            hasWildcard = true;
        }
      }

      variableDependencies.put(variable, dependencies);
      if (hasWildcard)
        variableWildcardDependencies.put(variable, dependencies);
    }

    // Remove wildcard dependencies, by replacing with match variable codes 
    for (Entry<Variable, Set<String>> entryWild : variableWildcardDependencies.entrySet()) {
      dependencies = new HashSet<String>();

      for (String dependency : entryWild.getValue()) {
        if (!dependency.contains("*")) {
          dependencies.add(dependency);
        } else {
          for (Entry<Variable, Set<String>> entryVar : variableDependencies.entrySet()) {
            if (entryVar.getKey().equals(entryWild.getKey()))
              continue; //Ignore it self
            if (wildcardMatch(entryVar.getKey().getCode(), dependency))
              dependencies.add(entryVar.getKey().getCode());
          }
        }
      }

      variableDependencies.put(entryWild.getKey(), dependencies);
    }

    List<Variable> selectedOutputVariables = new ArrayList<Variable>();
    List<String> selectedOutputVariableCodes = new ArrayList<String>();
    List<String> previousOutputVariableCodes = new ArrayList<String>();

    // Loop until variableDependencies collection have zero elements
    do {
      selectedOutputVariables.clear();
      selectedOutputVariableCodes.clear();

      // Filter Variables that have no dependencies. They can be added to the stack
      for (Entry<Variable, Set<String>> entry : variableDependencies.entrySet()) {
        //Remove previous sorted dependency codes
        dependencies = entry.getValue();
        dependencies.removeAll(previousOutputVariableCodes);

        //Update the dependencies list of the variable
        variableDependencies.put(entry.getKey(), dependencies);

        //Check if all dependencies are already sorted
        if (dependencies.size() == 0) {
          selectedOutputVariables.add(entry.getKey());
          selectedOutputVariableCodes.add(entry.getKey().getCode());
        }
      }

      //Sort by code, just to always have the same sequence (its easier when debugging)
      selectedOutputVariables.sort((s1, s2) -> s1.getCode().compareTo(s2.getCode()));

      //Add to the sorted list
      sortedOutputVariables.addAll(selectedOutputVariables);
      sortedOutputVariableCodes.addAll(selectedOutputVariableCodes);

      //Remove from the unsorted MAP
      variableDependencies.keySet().removeAll(selectedOutputVariables);

      //Move the selectedOutputVariables to the previousOutputVariableCodes
      previousOutputVariableCodes.clear();
      previousOutputVariableCodes.addAll(selectedOutputVariableCodes);

      if (selectedOutputVariables.size() == 0 && variableDependencies.size() > 0) {
        //Something is wrong. There's still variables with dependencies, but no variables were selected in the last loop.
        //OR we have circular references. OR there are references, that are not present in the list.
        PeriodProcessDependenciesNotSatisfyedException ex = new PeriodProcessDependenciesNotSatisfyedException(
            variableDependencies);
        ex.printDependencies();
        throw ex;
      }
    } while (variableDependencies.size() > 0);

    return sortedOutputVariables;
  }

  /**
   * Search a string {@code text} to see if it matches the given {@code pattern}.<br>
   * Example: {@code text = "010203"} , {@code pattern = "0102*"} , will return {@code true}
   * 
   * @param  text
   * @param  pattern
   * @return
   */
  public static boolean wildcardMatch(String text, String pattern) {
    // Replace '*' with '.*' (matches zero or more characters)
    // Replace '?' with '.' (matches exactly one character)
    String regex = pattern.replace("?", ".").replace("*", ".*");

    // Match the text against the regular expression
    return Pattern.matches(regex, text);
  }

  private List<FunctionCall> extractFunctionCalls(String mvelExpression, String targetFunction) {
    List<FunctionCall> functionCalls = new ArrayList<FunctionCall>();

    if (mvelExpression == null || mvelExpression.trim().length() == 0) {
      return functionCalls;
    }

    // Regular expression to match function calls
    String regex = "(\\w+)\\(([^)]*)\\)";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(mvelExpression);

    while (matcher.find()) {
      String functionName = matcher.group(1); // Function name
      String arguments = matcher.group(2);   // Argument list as a single string

      if (targetFunction != null && !functionName.equals(targetFunction)) {
        // Ignore this functions
        continue;
      }

      // Split arguments, considering commas and trim spaces
      List<String> args = splitArguments(arguments);

      functionCalls.add(new FunctionCall(functionName, args));
    }

    return functionCalls;
  }

  // Helper method to split arguments safely
  private static List<String> splitArguments(String arguments) {
    List<String> result = new ArrayList<>();

    int depth = 0;
    StringBuilder currentArg = new StringBuilder();

    for (char c : arguments.toCharArray()) {
      if (c == ',' && depth == 0) {
        // Argument separator at the top level
        result.add(currentArg.toString().trim());
        currentArg.setLength(0);
      } else {
        // Adjust depth for nested parentheses
        if (c == '(')
          depth++;
        if (c == ')')
          depth--;

        currentArg.append(c);
      }
    }

    // Add the last argument
    if (currentArg.length() > 0) {
      result.add(currentArg.toString().trim());
    }

    return result;
  }

  private String extractVariableCodeFromOutFunction(List<String> arguments) {
    int size = arguments.size();

    if (size >= 1) {
      //The code is always the first argument
      String code = arguments.get(0);

      //Remove the commas
      code = code.replace("\"", "");

      return code;
    }

    return null;
  }

  public void calculateOrganization(GenericGuide<Period> guide, Organization organization,
      List<Variable> outputVariables) {
    Period period = guide.getActivityDomain();
    PeriodVersion periodVersion = (PeriodVersion) guide.otherValues.get(
        PeriodProcessOutputsActivity.GUIDE_OTHERVALUE_PERIODVERSION);

    Integer countOutputVariables = outputVariables.size();
    Integer calculatedOutputVariables = 0;
    String progressTaskText = guide.getActivityProgressDescriptor().getProgressTask();
    Integer progressPercentage = guide.getActivityProgressDescriptor().getProgressPercentage();
    Integer progressOrgPercentage = (Integer) guide.otherValues.get("PROGRESS_ORG");
    Integer progressOutputPercentage;

    String orgProgressTaskText = progressTaskText + " - Calculating " + organization.getCode();
    guide.getActivityProgressDescriptor().updateProgress(progressPercentage, orgProgressTaskText);

    OutputVariableExpressionFunctions expressionFunctions = new OutputVariableExpressionFunctions(period, periodVersion,
        organization);
    applicationContext.getAutowireCapableBeanFactory().autowireBean(expressionFunctions); //This is CRUCIAL, Without this there will be no dependency injection

    for (Variable variable : outputVariables) {
      self().calculateOutput(variable, period, periodVersion, organization, expressionFunctions);

      // Progress
      calculatedOutputVariables++;
      progressOutputPercentage = calculatedOutputVariables * progressOrgPercentage / outputVariables.size();
      guide.getActivityProgressDescriptor().updateProgress(progressPercentage + progressOutputPercentage,
          orgProgressTaskText + " [" + calculatedOutputVariables + " / " + countOutputVariables + "]");
    }
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void calculateOutput(Variable outputVariable, Period period, PeriodVersion periodVersion,
      Organization organization, OutputVariableExpressionFunctions expressionFunctions) {
    // Evaluate outputFormula
    OutputVariableExpressionContext context = OutputVariableExpressionContext.create(emissionFactorRepository,
        inputDataRepository, outputVariable, period, periodVersion, organization);
    context.setExpressionFunctions(expressionFunctions);

    Object result;
    String expression = outputVariable.getOutputFormula();
    try {
      result = ExpressionUtils.evalExpression(expression, context);
    } catch (ExpressionEvaluationException e) {
      throw new OutputExpressionEvaluationRuntimeException(outputVariable.getCode(), organization.getCode(),
          period.getName(), periodVersion.getName(), expression, e);
    }

    if (result == null) {
      throw new OutputExpressionNullResultRuntimeException(outputVariable.getCode(), organization.getCode(),
          period.getName(), periodVersion.getName(), expression);
    }

    // Insert output, in new transation
    OutputData outputData = new OutputData().baseUnit(outputVariable.getBaseUnit()).owner(organization).period(
        period).periodVersion(periodVersion).value(
            ExpressionUtils.toBigDecimal(result, outputVariable.getValueScale())).variable(outputVariable);
    try {
      outputDataRepository.save(outputData);
    } catch (Throwable t) {
      // Something happened when saving the outputData. Log the record for debugging
      log.error("Error saving OutputData : " + outputData.toString(), t);
      throw t;
    }
  }

}
