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

import de.avatic.lcc.dto.generic.ContainerType;
import de.avatic.lcc.dto.generic.RateType;
import de.avatic.lcc.model.calculation.*;
import de.avatic.lcc.model.db.calculations.*;
import de.avatic.lcc.model.db.error.SysError;
import de.avatic.lcc.model.db.error.SysErrorType;
import de.avatic.lcc.model.db.packaging.LoadCarrierType;
import de.avatic.lcc.model.db.packaging.PackagingDimension;
import de.avatic.lcc.model.db.premises.Premise;
import de.avatic.lcc.model.db.premises.route.Destination;
import de.avatic.lcc.model.db.premises.route.RouteSection;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
import de.avatic.lcc.repositories.error.SysErrorRepository;
import de.avatic.lcc.repositories.premise.DestinationRepository;
import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.premise.RouteRepository;
import de.avatic.lcc.repositories.premise.RouteSectionRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.calculation.execution.steps.*;
import de.avatic.lcc.service.transformer.error.SysErrorTransformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.CompletableFuture;

@Service
public class CalculationExecutionService {


    private static final Logger log = LoggerFactory.getLogger(CalculationExecutionService.class);
    private final PremiseRepository premiseRepository;
    private final DestinationRepository destinationRepository;
    private final RouteRepository routeRepository;
    private final RouteSectionRepository routeSectionRepository;
    private final CustomCostCalculationService customCostCalculationService;
    private final RouteSectionCostCalculationService routeSectionCostCalculationService;
    private final HandlingCostCalculationService handlingCostCalculationService;
    private final InventoryCostCalculationService inventoryCostCalculationService;
    private final PropertyRepository propertyRepository;
    private final AirfreightCalculationService airfreightCalculationService;
    private final PremiseToHuService premiseToHuService;
    private final ContainerCalculationService containerCalculationService;
    private final ShippingFrequencyCalculationService shippingFrequencyCalculationService;
    private final CalculationJobRepository calculationJobRepository;
    private final SysErrorRepository sysErrorRepository;
    private final SysErrorTransformer sysErrorTransformer;


    public CalculationExecutionService(PremiseRepository premiseRepository, DestinationRepository destinationRepository, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, CustomCostCalculationService customCostCalculationService, RouteSectionCostCalculationService routeSectionCostCalculationService, HandlingCostCalculationService handlingCostCalculationService, InventoryCostCalculationService inventoryCostCalculationService, PropertyRepository propertyRepository, AirfreightCalculationService airfreightCalculationService, PremiseToHuService premiseToHuService, ContainerCalculationService containerCalculationService, ShippingFrequencyCalculationService shippingFrequencyCalculationService, CalculationJobRepository calculationJobRepository, SysErrorRepository sysErrorRepository, SysErrorTransformer sysErrorTransformer) {
        this.premiseRepository = premiseRepository;
        this.destinationRepository = destinationRepository;
        this.routeRepository = routeRepository;
        this.routeSectionRepository = routeSectionRepository;
        this.customCostCalculationService = customCostCalculationService;
        this.routeSectionCostCalculationService = routeSectionCostCalculationService;
        this.handlingCostCalculationService = handlingCostCalculationService;
        this.inventoryCostCalculationService = inventoryCostCalculationService;
        this.propertyRepository = propertyRepository;
        this.airfreightCalculationService = airfreightCalculationService;
        this.premiseToHuService = premiseToHuService;
        this.containerCalculationService = containerCalculationService;
        this.shippingFrequencyCalculationService = shippingFrequencyCalculationService;
        this.calculationJobRepository = calculationJobRepository;
        this.sysErrorRepository = sysErrorRepository;
        this.sysErrorTransformer = sysErrorTransformer;
    }

    private static ContainerType getBestContainerType(Map<ContainerType, List<SectionInfo>> sectionResults) {
        return sectionResults.entrySet().stream()
                .min(Comparator.comparing(entry ->
                        entry.getValue().stream()
                                .map(SectionInfo::result)
                                .map(CalculationJobRouteSection::getAnnualCost)
                                .reduce(BigDecimal.ZERO, BigDecimal::add)))
                .map(Map.Entry::getKey)
                .orElse(null);
    }


    @Async("calculationExecutor")
    public CompletableFuture<CalculationResult> launchJobCalculation(Integer calculationId) {

        var optCalc = calculationJobRepository.getCalculationJob(calculationId);

        try {
            return CompletableFuture.completedFuture(new CalculationResult(calculationId, calculateJob(optCalc.orElseThrow())));
        } catch (Throwable e) {

            var calc = optCalc.orElse(null);

            var error = new SysError();
            error.setType(SysErrorType.CALCULATION);
            error.setCode(e.getClass().getSimpleName().substring(0, Math.min(e.getClass().getSimpleName().length(), 253)));
            error.setTitle("Calculation operation execution " + calculationId + " failed");

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

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

            sysErrorRepository.insert(error);

            return CompletableFuture.completedFuture(new CalculationResult(calculationId, e));
        }
    }

    public List<DestinationInfo> calculateJob(CalculationJob calculation) {

        if (CalculationJobState.SCHEDULED.equals(calculation.getJobState())) {
            Premise premise = premiseRepository.getPremiseById(calculation.getPremiseId()).orElseThrow();

            // material cost + fca cost
            var materialCost = premise.getMaterialCost();
            BigDecimal fcaFee;

            if (premise.getFcaEnabled()) {
                var fcaProperty = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FCA_FEE, calculation.getPropertySetId()).orElseThrow();
                var fcaShare = Double.parseDouble(fcaProperty.getCurrentValue());
                fcaFee = BigDecimal.valueOf(fcaShare).multiply(materialCost);
            } else {
                fcaFee = BigDecimal.ZERO;
            }

            List<Destination> destinations = destinationRepository.getByPremiseId(premise.getId());
            return destinations.stream().map(destination -> doDestinationCalculation(calculation.getPropertySetId(), calculation.getValidityPeriodId(), destination, premise, materialCost, fcaFee)).toList();
        }

        throw new IllegalStateException("Calculation job is not scheduled");
    }

    private DestinationInfo doDestinationCalculation(Integer setId, Integer periodId, Destination destination, Premise premise, BigDecimal materialCost, BigDecimal fcaFee) {

        InventoryCostResult inventoryCost;
        HandlingResult handlingCost;
        CustomResult customCost;
        List<SectionInfo> sections;
        AirfreightResult airfreightCost = airfreightCalculationService.doCalculation(setId, periodId, premise, destination);
        ContainerType usedContainerType = null;

        CalculationJobDestination destinationCalculationJob = new CalculationJobDestination();
        boolean hasMainRun = true;
        BigDecimal leadTime = null;

        if (destination.getD2d()) {
            var containerCalculation = containerCalculationService.doCalculation(setId, premiseToHuService.createHuFromPremise(premise), ContainerType.FEU);
            sections = List.of(new SectionInfo(null, routeSectionCostCalculationService.doD2dCalculation(setId, periodId, premise, destination, containerCalculation), containerCalculation));
            leadTime = BigDecimal.valueOf(destination.getLeadTimeD2d());
            usedContainerType = ContainerType.FEU;
        } else {
            var bestContainerTypeResult = getSectionsFromBestContainerType(setId, periodId, destination, premise);
            sections = bestContainerTypeResult.sections;
            usedContainerType = bestContainerTypeResult.containerType;
            hasMainRun = sections.stream().anyMatch(s -> s.section().getMainRun());
            leadTime = BigDecimal.valueOf(sections.stream().mapToInt(s -> s.result().getTransitTime()).sum());

            /* inner eu routing */
            if(!hasMainRun) {
                sections.forEach(s -> {
                    s.result().setMainRun(false);
                    s.result().setPreRun(false);
                    s.result().setPostRun(false);
                });
            }
        }

        destinationCalculationJob.setD2D(destination.getD2d());

        if(destination.getD2d())
            destinationCalculationJob.setRateD2D(destination.getRateD2d());

        customCost = customCostCalculationService.doCalculation(setId, premise, destination, sections);
        handlingCost = handlingCostCalculationService.doCalculation(setId, premise, destination, hasMainRun);
        inventoryCost = inventoryCostCalculationService.doCalculation(setId, premise, destination, leadTime);

        destinationCalculationJob.setContainerType(usedContainerType);

        destinationCalculationJob.setMaterialCost(materialCost);
        destinationCalculationJob.setFcaCost(fcaFee);

        destinationCalculationJob.setAirFreightShare(airfreightCost.getAirFreightShare());
        destinationCalculationJob.setAirFreightShareMax(airfreightCost.getMaxAirfreightShare());
        destinationCalculationJob.setAirFreightWeight(airfreightCost.getWeight());
        destinationCalculationJob.setAirFreightVolumetricWeight(airfreightCost.getVolumetricWeight());
        destinationCalculationJob.setAnnualAirFreightCost(airfreightCost.getAnnualCost());

        destinationCalculationJob.setSmallUnit(handlingCost.getLoadCarrierType().equals(LoadCarrierType.SLC));
        destinationCalculationJob.setAnnualHandlingCost(handlingCost.getAnnualHandlingCost());
        destinationCalculationJob.setAnnualRepackingCost(handlingCost.getAnnualRepackingCost());
        destinationCalculationJob.setAnnualDisposalCost(handlingCost.getAnnualDisposalCost());

        destinationCalculationJob.setStockedInventory(inventoryCost.getStockedInventory());
        destinationCalculationJob.setInTransportStock(inventoryCost.getInTransportStock());
        destinationCalculationJob.setSafetyStockInDays(inventoryCost.getSafetyStockInDays());
        destinationCalculationJob.setSafetyStock(inventoryCost.getSafetyStock());
        destinationCalculationJob.setStockBeforePayment(inventoryCost.getStockBeforePayment());
        destinationCalculationJob.setOperationalStock(inventoryCost.getOperationalStock());
        destinationCalculationJob.setAnnualCapitalCost(inventoryCost.getAnnualCapitalCost());
        destinationCalculationJob.setAnnualStorageCost(inventoryCost.getAnnualStorageCost());

        destinationCalculationJob.setCustomDuties(customCost.getCustomDuties());
        destinationCalculationJob.setCustomValue(customCost.getCustomValue());
        destinationCalculationJob.setTariffRate(customCost.getTariffRate());
        destinationCalculationJob.setAnnualCustomCost(customCost.getAnnualCost());

        destinationCalculationJob.setAnnualTransportationCost(sections.stream().map(SectionInfo::result).map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add));
        destinationCalculationJob.setTransportWeightExceeded(sections.stream().map(SectionInfo::result).filter(CalculationJobRouteSection::getMainRun).anyMatch(CalculationJobRouteSection::isWeightPrice));
        destinationCalculationJob.setLayerCount(sections.getFirst().containerResult().getLayer());
        destinationCalculationJob.setLayerStructure(null); //TODO generate layer structure
        destinationCalculationJob.setHuCount(sections.getFirst().containerResult().getHuUnitCount());

        destinationCalculationJob.setAnnualAmount(BigDecimal.valueOf(destination.getAnnualAmount()));
        destinationCalculationJob.setShippingFrequency(shippingFrequencyCalculationService.doCalculation(setId, destination.getAnnualAmount()));

        var commonCost = destinationCalculationJob.getAnnualHandlingCost()
                .add(destinationCalculationJob.getAnnualDisposalCost())
                .add(destinationCalculationJob.getAnnualCapitalCost())
                .add(destinationCalculationJob.getAnnualStorageCost())
                .add(materialCost.multiply(BigDecimal.valueOf(destination.getAnnualAmount())))
                .add(fcaFee.multiply(BigDecimal.valueOf(destination.getAnnualAmount())));

        var totalCost = commonCost.add(destinationCalculationJob.getAnnualTransportationCost()).add(destinationCalculationJob.getAnnualCustomCost());
        var totalRiskCost = commonCost.add(customCost.getAnnualRiskCost()).add(sections.stream().map(SectionInfo::result).map(CalculationJobRouteSection::getAnnualRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add));
        var totalChanceCost = commonCost.add(customCost.getAnnualChanceCost()).add(sections.stream().map(SectionInfo::result).map(CalculationJobRouteSection::getAnnualChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add));

        destinationCalculationJob.setTotalCost(totalCost);
        destinationCalculationJob.setTotalRiskCost(totalRiskCost);
        destinationCalculationJob.setTotalChanceCost(totalChanceCost);

        destinationCalculationJob.setTotalTransitTime(sections.stream().mapToInt(s -> s.result().getTransitTime()).sum());
        destinationCalculationJob.setContainerUtilization(sections.stream().map(s -> s.result().getUtilization()).reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(sections.size()), 10, RoundingMode.HALF_UP));

        destinationCalculationJob.setPremiseDestinationId(destination.getId());


        return new DestinationInfo(destination, destinationCalculationJob, sections);
    }

    private BestContainerTypeResult getSectionsFromBestContainerType(Integer setId, Integer periodId, Destination destination, Premise premise) {
        PackagingDimension hu = premiseToHuService.createHuFromPremise(premise);

        var route = routeRepository.getSelectedByDestinationId(destination.getId()).orElseThrow();
        List<RouteSection> routeSections = routeSectionRepository.getByRouteId(route.getId());

        Map<ContainerType, List<SectionInfo>> sectionInfos = new HashMap<>();
        Map<ContainerType, ContainerCalculationResult> containerCalculation = new HashMap<>();

        // Get container calculation
        for (var containerType : ContainerType.values()) {
            containerCalculation.put(containerType, containerCalculationService.doCalculation(setId, hu, containerType));
        }

        for (var containerType : ContainerType.values()) {
            if (!containerType.equals(ContainerType.TRUCK)) {

                var sectionInfo = new ArrayList<SectionInfo>();

                for (var section : routeSections) {
                    var container = RateType.MATRIX == section.getRateType() ? containerCalculation.get(ContainerType.TRUCK) : containerCalculation.get(containerType);
                    sectionInfo.add(new SectionInfo(section, routeSectionCostCalculationService.doCalculation(setId, periodId, premise, destination, section, container), containerCalculation.get(containerType)));
                }

                sectionInfos.put(containerType, sectionInfo);
            }

        }
        var bestContainerType = getBestContainerType(sectionInfos);
        return new BestContainerTypeResult(bestContainerType, sectionInfos.get(bestContainerType));
    }

    private record BestContainerTypeResult(ContainerType containerType, List<SectionInfo> sections) {
    }

}
