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

import de.avatic.lcc.calculationmodel.*;
import de.avatic.lcc.dto.generic.ContainerType;
import de.avatic.lcc.dto.generic.RateType;
import de.avatic.lcc.model.calculations.CalculationJob;
import de.avatic.lcc.model.calculations.CalculationJobDestination;
import de.avatic.lcc.model.calculations.CalculationJobRouteSection;
import de.avatic.lcc.model.calculations.CalculationJobState;
import de.avatic.lcc.model.packaging.LoadCarrierType;
import de.avatic.lcc.model.packaging.PackagingDimension;
import de.avatic.lcc.model.premises.Premise;
import de.avatic.lcc.model.premises.route.Destination;
import de.avatic.lcc.model.premises.route.RouteSection;
import de.avatic.lcc.model.properties.SystemPropertyMappingId;
import de.avatic.lcc.repositories.calculation.CalculationJobDestinationRepository;
import de.avatic.lcc.repositories.calculation.CalculationJobRepository;
import de.avatic.lcc.repositories.calculation.CalculationJobRouteSectionRepository;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

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

@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 CalculationJobDestinationRepository calculationJobDestinationRepository;
    private final CalculationJobRouteSectionRepository calculationJobRouteSectionRepository;


    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, CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository) {
        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.calculationJobDestinationRepository = calculationJobDestinationRepository;
        this.calculationJobRouteSectionRepository = calculationJobRouteSectionRepository;
    }

    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);
    }

    public void calculateJob(Integer calculationId) {

        CalculationJob calculation = calculationJobRepository.getCalculationJob(calculationId).orElseThrow();

        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).orElseThrow();
                var fcaShare = Double.parseDouble(fcaProperty.getCurrentValue());
                fcaFee = BigDecimal.valueOf(fcaShare).multiply(materialCost);
            } else {
                fcaFee = BigDecimal.ZERO;
            }

            List<Destination> destinations = destinationRepository.getByPremiseId(premise.getId());
            var destinationInfos = destinations.stream().map(destination -> doDestinationCalculation(destination, premise, materialCost, fcaFee)).toList();

            for (var destinationInfo : destinationInfos) {
                destinationInfo.destinationCalculationJob.setCalculationJobId(calculation.getId());
                var destinationId = calculationJobDestinationRepository.insert(destinationInfo.destinationCalculationJob);

                for (var sectionInfo : destinationInfo.sectionInfo) {
                    var section = sectionInfo.result();
                    section.setCalculationJobDestinationId(destinationId);
                    calculationJobRouteSectionRepository.insert(section);
                }
            }


            log.info("Calculation job {} finished", calculationId);
        }


    }

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

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

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

        if (destination.getD2d()) {
            var containerCalculation = containerCalculationService.doCalculation(premiseToHuService.createHuFromPremise(premise), ContainerType.FEU);
            sections = List.of(new SectionInfo(null, routeSectionCostCalculationService.doD2dCalculation(premise, destination, containerCalculation), containerCalculation));
            leadTime = BigDecimal.valueOf(destination.getLeadTimeD2d());
            usedContainerType = ContainerType.FEU;
        } else {
            var bestContainerTypeResult = getSectionsFromBestContainerType(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());
        }

        customCost = customCostCalculationService.doCalculation(premise, destination, sections);
        handlingCost = handlingCostCalculationService.doCalculation(premise, destination, hasMainRun);
        inventoryCost = inventoryCostCalculationService.doCalculation(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).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(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(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(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(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) {
    }

    private record DestinationInfo(Destination destination, CalculationJobDestination destinationCalculationJob,
                                   List<SectionInfo> sectionInfo) {
    }
}
