package com.oguerreiro.resilient.service;

import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.domain.Period;
import com.oguerreiro.resilient.domain.Unit;
import com.oguerreiro.resilient.domain.UnitConverter;
import com.oguerreiro.resilient.domain.Variable;
import com.oguerreiro.resilient.domain.VariableUnits;
import com.oguerreiro.resilient.domain.enumeration.UnitValueType;
import com.oguerreiro.resilient.error.ExpressionEvaluationException;
import com.oguerreiro.resilient.error.UnitConverterExpressionEvaluationRuntimeException;
import com.oguerreiro.resilient.error.VariableUnitConverterMoreThanOneFoundException;
import com.oguerreiro.resilient.error.VariableUnitConverterNotFoundException;
import com.oguerreiro.resilient.error.VariableUnitConverterWithoutFormulaException;
import com.oguerreiro.resilient.expression.ExpressionUtils;
import com.oguerreiro.resilient.expression.UnitConverterExpressionContext;
import com.oguerreiro.resilient.expression.UnitConverterExpressionFunctions;
import com.oguerreiro.resilient.repository.EmissionFactorRepository;
import com.oguerreiro.resilient.repository.PeriodRepository;
import com.oguerreiro.resilient.repository.UnitConverterRepository;
import com.oguerreiro.resilient.repository.UnitRepository;
import com.oguerreiro.resilient.repository.VariableRepository;
import com.oguerreiro.resilient.service.dto.UnitConverterDTO;
import com.oguerreiro.resilient.service.interceptors.BeforeSaveServiceInterceptor;
import com.oguerreiro.resilient.service.interceptors.BeforeUpdateServiceInterceptor;
import com.oguerreiro.resilient.service.mapper.UnitConverterMapper;

/**
 * Service Implementation for managing
 * {@link com.oguerreiro.resilient.domain.UnitConverter}.
 */
@Service
@Transactional
public class UnitConverterService extends AbstractResilientService<UnitConverterDTO, UnitConverter, Long>
    implements BeforeSaveServiceInterceptor<UnitConverterDTO, UnitConverter, Long>,
    BeforeUpdateServiceInterceptor<UnitConverterDTO, UnitConverter, Long> {

  private final UnitConverterRepository unitConverterRepository;
  private final UnitConverterMapper unitConverterMapper;
  private final EmissionFactorRepository emissionFactorRepository;
  private final ApplicationContext applicationContext;

  private final PeriodRepository periodRepository;
  private final VariableRepository variableRepository;
  private final UnitRepository unitRepository;

  public UnitConverterService(UnitConverterRepository unitConverterRepository, UnitConverterMapper unitConverterMapper,
      EmissionFactorRepository emissionFactorRepository, ApplicationContext applicationContext,
      PeriodRepository periodRepository, VariableRepository variableRepository, UnitRepository unitRepository) {
    super(UnitConverter.class, unitConverterRepository, unitConverterMapper);

    this.unitConverterRepository = unitConverterRepository;
    this.unitConverterMapper = unitConverterMapper;
    this.emissionFactorRepository = emissionFactorRepository;
    this.applicationContext = applicationContext;

    this.periodRepository = periodRepository;
    this.variableRepository = variableRepository;
    this.unitRepository = unitRepository;
  }

  @Override
  public UnitConverter beforeSave(UnitConverter domain) {
    // Validate expression. Syntax only.
    ExpressionUtils.validateExpression(domain.getConvertionFormula(),
        UnitConverterExpressionContext.create(emissionFactorRepository));

    return domain;
  }

  @Override
  public UnitConverter beforeUpdate(UnitConverter domain) {
    // Validate expression. Syntax only.
    ExpressionUtils.validateExpression(domain.getConvertionFormula(),
        UnitConverterExpressionContext.create(emissionFactorRepository));

    return domain;
  }

  @Transactional(readOnly = true)
  public List<UnitConverterDTO> findAllFromAndTo(Long fromUnitId, Long toUnitId) {
    log.debug("Request to get a list of UnitConverters that math {fromUnitId} and {toUnitId}");
    return unitConverterRepository.findAllFromAndTo(fromUnitId, toUnitId).stream().map(
        unitConverterMapper::toDto).collect(Collectors.toCollection(LinkedList::new));
  }

  public BigDecimal convertIfNeeded(Long periodId, Long variableId, Long sourceUnitId, BigDecimal value,
      String variableClassCode) throws VariableUnitConverterNotFoundException,
      VariableUnitConverterWithoutFormulaException, VariableUnitConverterMoreThanOneFoundException {
    Period period = periodRepository.getReferenceById(periodId);
    Variable variable = variableRepository.getReferenceById(variableId);
    Unit sourceUnit = unitRepository.getReferenceById(sourceUnitId);

    return convertIfNeeded(period, variable, sourceUnit, value, variableClassCode);
  }

  public BigDecimal convertIfNeeded(Period period, Variable variable, Unit sourceUnit, BigDecimal value,
      String variableClassCode) throws VariableUnitConverterNotFoundException,
      VariableUnitConverterWithoutFormulaException, VariableUnitConverterMoreThanOneFoundException {
    if (sourceUnit.getUnitType().getValueType().equals(UnitValueType.STRING))
      throw new RuntimeException("UnitValueType STRING has no convertion.");

    BigDecimal convertedValue = null;

    if (sourceUnit.equals(variable.getBaseUnit())) {
      // Its the same. No need to convert value.
      convertedValue = value;
    } else {
      // Conversion is needed. The units are both of the same UniType ?
      if (sourceUnit.getUnitType().equals(variable.getBaseUnit().getUnitType())) {
        // Same UnitType, the conversion is just a scale conversion
        // (sourceValue*CR(target unit)/CR(source unit))
        convertedValue = value.multiply(
            variable.getBaseUnit().getConvertionRate().divide(sourceUnit.getConvertionRate()));
      } else {
        // This is a more complex conversion. Needs a UnitConverter
        // First: Is this unit allowed in Variable?
        List<VariableUnits> sourceVariableUnits = variable.getVariableUnits().stream().filter(
            vUnit -> vUnit.getUnit().equals(sourceUnit)).toList();
        if (sourceVariableUnits.isEmpty()) {
          throw new VariableUnitConverterNotFoundException(variable.getCode(), sourceUnit.getCode(),
              variable.getBaseUnit().getCode());
        } else if (sourceVariableUnits.size() > 1) {
          throw new VariableUnitConverterMoreThanOneFoundException(variable.getCode(), sourceUnit.getCode(),
              variable.getBaseUnit().getCode());
        }

        VariableUnits variableUnit = sourceVariableUnits.get(0);
        UnitConverter unitConverter = variableUnit.getUnitConverter();

        // Unit converter without conversion formula
        if (unitConverter.getConvertionFormula() == null || unitConverter.getConvertionFormula().isBlank()) {
          throw new VariableUnitConverterWithoutFormulaException(variable.getCode(), sourceUnit.getCode(),
              variable.getBaseUnit().getCode());
        }

        // Evaluate formula to convert the value
        UnitConverterExpressionFunctions expressionFunctions = new UnitConverterExpressionFunctions(period);
        applicationContext.getAutowireCapableBeanFactory().autowireBean(expressionFunctions); //This is CRUCIAL, Without this there will be no dependency injection

        UnitConverterExpressionContext context = UnitConverterExpressionContext.create(emissionFactorRepository,
            period.getBeginDate().getYear(), value, sourceUnit, variable.getBaseUnit(), variable, variableClassCode);
        context.setExpressionFunctions(expressionFunctions);

        String convertionFormula = unitConverter.getConvertionFormula();

        try {
          Object resultValue = ExpressionUtils.evalExpression(convertionFormula, context);
          convertedValue = ExpressionUtils.toBigDecimal(resultValue);
        } catch (ExpressionEvaluationException e) {
          throw new UnitConverterExpressionEvaluationRuntimeException(variable.getCode(), sourceUnit.getCode(),
              variable.getBaseUnit().getCode(), value.toString(), e);
        }
      }
    }

    return convertedValue;
  }
}
