package de.avatic.lcc.service.transformer.report;

import de.avatic.lcc.dto.generic.NodeType;
import de.avatic.lcc.dto.generic.RateType;
import de.avatic.lcc.dto.report.ReportDTO;
import de.avatic.lcc.dto.report.ReportDestinationDTO;
import de.avatic.lcc.dto.report.ReportEntryDTO;
import de.avatic.lcc.dto.report.ReportSectionDTO;
import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.model.db.calculations.CalculationJobDestination;
import de.avatic.lcc.model.db.calculations.CalculationJobRouteSection;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.model.db.premises.Premise;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.repositories.MaterialRepository;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.calculation.CalculationJobDestinationRepository;
import de.avatic.lcc.repositories.calculation.CalculationJobRouteSectionRepository;
import de.avatic.lcc.repositories.premise.PremiseRepository;
import de.avatic.lcc.repositories.premise.RouteNodeRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.repositories.properties.PropertySetRepository;
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.transformer.generic.MaterialTransformer;
import de.avatic.lcc.service.transformer.generic.NodeTransformer;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class ReportTransformer {

    private final CalculationJobDestinationRepository calculationJobDestinationRepository;
    private final CalculationJobRouteSectionRepository calculationJobRouteSectionRepository;
    private final PremiseRepository premiseRepository;
    private final NodeRepository nodeRepository;
    private final NodeTransformer nodeTransformer;
    private final RouteNodeRepository routeNodeRepository;
    private final PropertyRepository propertyRepository;
    private final UserNodeRepository userNodeRepository;
    private final MaterialRepository materialRepository;
    private final MaterialTransformer materialTransformer;
    private final PropertySetRepository propertySetRepository;
    private final ValidityPeriodRepository validityPeriodRepository;

    public ReportTransformer(CalculationJobDestinationRepository calculationJobDestinationRepository, CalculationJobRouteSectionRepository calculationJobRouteSectionRepository, PremiseRepository premiseRepository, NodeRepository nodeRepository, NodeTransformer nodeTransformer, RouteNodeRepository routeNodeRepository, PropertyRepository propertyRepository, UserNodeRepository userNodeRepository, MaterialRepository materialRepository, MaterialTransformer materialTransformer, PropertySetRepository propertySetRepository, ValidityPeriodRepository validityPeriodRepository) {
        this.calculationJobDestinationRepository = calculationJobDestinationRepository;
        this.calculationJobRouteSectionRepository = calculationJobRouteSectionRepository;
        this.premiseRepository = premiseRepository;
        this.nodeRepository = nodeRepository;
        this.nodeTransformer = nodeTransformer;
        this.routeNodeRepository = routeNodeRepository;
        this.propertyRepository = propertyRepository;
        this.userNodeRepository = userNodeRepository;
        this.materialRepository = materialRepository;
        this.materialTransformer = materialTransformer;
        this.propertySetRepository = propertySetRepository;
        this.validityPeriodRepository = validityPeriodRepository;
    }

    public ReportDTO toReportDTO(CalculationJob job) {

        ReportDTO reportDTO = new ReportDTO();

        var reportingProperty = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.REPORTING).orElseThrow();
        boolean includeAirfreight = reportingProperty.getCurrentValue().equals("MEK_C");

        List<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId());
        Map<Integer, List<CalculationJobRouteSection>> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList());

        var weightedTotalCost = getWeightedTotalCosts(sections);

        Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
        reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getByIdIncludeDeprecated(premise.getMaterialId()).orElseThrow()));

        var period = getPeriod(job);

        reportDTO.setStartDate(period.startDate);
        reportDTO.setEndDate(period.endDate);

        Node sourceNode = null;

        if (premise.getSupplierNodeId() != null)
            sourceNode = nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();

        if (premise.getUserSupplierNodeId() != null)
            sourceNode = userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow();


        if (!destinations.isEmpty()) {
            reportDTO.setCost(getCostMap(job, destinations, weightedTotalCost, includeAirfreight));
            reportDTO.setRisk(getRisk(job, destinations, weightedTotalCost, includeAirfreight));
            Node finalSourceNode = sourceNode;
            reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise, finalSourceNode, includeAirfreight)).toList());
        } else {
            throw new InternalErrorException("Malformed calculation. No destinations found.");
        }

        if (reportDTO.getDestinations() != null && !reportDTO.getDestinations().isEmpty()) {

            var source = reportDTO.getDestinations().getFirst().getSections().stream().map(ReportSectionDTO::getFromNode).filter(n -> n.getTypes().contains(NodeType.SOURCE)).findFirst().orElseThrow();
            reportDTO.setSupplier(source);
        }

        return reportDTO;

    }

    private TimePeriod getPeriod(CalculationJob job) {
        var propertySet = propertySetRepository.getById(job.getPropertySetId());
        var validityPeriod = validityPeriodRepository.getById(job.getValidityPeriodId());

        var startDate = propertySet.getStartDate().isBefore(validityPeriod.getStartDate()) ? validityPeriod.getStartDate() : propertySet.getStartDate();

        if (propertySet.getEndDate() == null)
            return new TimePeriod(startDate, validityPeriod.getEndDate());

        if (validityPeriod.getEndDate() == null)
            return new TimePeriod(startDate, propertySet.getEndDate());

        var endDate = propertySet.getEndDate().isAfter(validityPeriod.getEndDate()) ? validityPeriod.getEndDate() : propertySet.getEndDate();

        return new TimePeriod(startDate, endDate);
    }

    private WeightedTotalCosts getWeightedTotalCosts(Map<Integer, List<CalculationJobRouteSection>> sectionsMap) {

        BigDecimal totalPreRunCost = BigDecimal.ZERO;
        BigDecimal totalMainRunCost = BigDecimal.ZERO;
        BigDecimal totalPostRunCost = BigDecimal.ZERO;
        BigDecimal totalD2D = BigDecimal.ZERO;


        BigDecimal totalCost = BigDecimal.ZERO;

        for (List<CalculationJobRouteSection> sections : sectionsMap.values()) {
            for (CalculationJobRouteSection section : sections) {

                if (section.getPreRun())
                    totalPreRunCost = totalPreRunCost.add(section.getAnnualCost());
                else if (section.getMainRun())
                    totalMainRunCost = totalMainRunCost.add(section.getAnnualCost());
                else if (section.getPostRun())
                    totalPostRunCost = totalPostRunCost.add(section.getAnnualCost());
                else if (section.getRateType().equals(RateType.D2D))
                    totalD2D = totalD2D.add(section.getAnnualCost());


                totalCost = totalCost.add(section.getAnnualCost());
            }
        }

        return new WeightedTotalCosts(totalPreRunCost, totalMainRunCost, totalPostRunCost, totalD2D, totalCost);
    }

    private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise, Node sourceNode, boolean includeAirfreight) {
        var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow();

        var dimensionUnit = premise.getHuDisplayedDimensionUnit();
        var weightUnit = premise.getHuDisplayedWeightUnit();

        ReportDestinationDTO destinationDTO = new ReportDestinationDTO();
        destinationDTO.setSections(sections.stream().map(s -> getSection(s, sourceNode, destinationNode, premise)).toList());

        var totalAnnualCost = sections.stream().map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        destinationDTO.getSections().forEach(s -> {
            s.getCost().setPercentage(s.getCost().getTotal().doubleValue() / totalAnnualCost.doubleValue());
        });
        destinationDTO.getSections().forEach(s -> {
            s.getDuration().setPercentage(s.getDuration().getTotal().doubleValue() / totalAnnualCost.doubleValue());
        });

        destinationDTO.setId(destination.getId());
        destinationDTO.setAnnualQuantity(destination.getAnnualAmount().intValue());

        destinationDTO.setDestination(nodeTransformer.toNodeDTO(destinationNode));

        destinationDTO.setDimensionUnit(dimensionUnit);
        destinationDTO.setWeightUnit(weightUnit);
        destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()));
        destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()));
        destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()));
        destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()));
        destinationDTO.setHuUnitCount(premise.getHuUnitCount());

        CalculationJobRouteSection mainRun = sections.stream().filter(CalculationJobRouteSection::getMainRun).findFirst().orElse(null);

        destinationDTO.setLayer(destination.getLayerCount());

        destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue());
        destinationDTO.setSafetyStock(destination.getSafetyStockInDays().intValue());
        destinationDTO.setTransportTime(destination.getTotalTransitTime().doubleValue());

        if (includeAirfreight)
            destinationDTO.setAirFreightShare(destination.getAirFreightShare().doubleValue());


        destinationDTO.setMixed(premise.getHuMixable());
        destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate());
        destinationDTO.setType(destination.getContainerType());
        destinationDTO.setUtilization(destination.getContainerUtilization());
        destinationDTO.setUnitCount(destination.getHuCount());
        destinationDTO.setWeightExceeded(destination.getTransportWeightExceeded());

        destinationDTO.setHsCode(premise.getHsCode());
        destinationDTO.setTariffRate(destination.getTariffRate());

        return destinationDTO;
    }

    private ReportSectionDTO getSection(CalculationJobRouteSection section, Node sourceNode, Node destinationNode, Premise premise) {
        ReportSectionDTO sectionDTO = new ReportSectionDTO();

        sectionDTO.setId(section.getId());
        sectionDTO.setTransportType(section.getTransportType());

        if (!section.getRateType().equals(RateType.D2D)) {
            sectionDTO.setFromNode(nodeTransformer.toNodeDTO(routeNodeRepository.getFromNodeBySectionId(section.getPremiseRouteSectionId()).orElseThrow()));
            sectionDTO.setToNode(nodeTransformer.toNodeDTO(routeNodeRepository.getToNodeBySectionId(section.getPremiseRouteSectionId()).orElseThrow()));
        } else {
            sectionDTO.setFromNode(nodeTransformer.toNodeDTO(sourceNode));
            sectionDTO.setToNode(nodeTransformer.toNodeDTO(destinationNode));
        }

        sectionDTO.setRateType(section.getRateType());


        var distance = new ReportEntryDTO();
        distance.setTotal(section.getDistance());
        sectionDTO.setDistance(distance);

        var duration = new ReportEntryDTO();
        duration.setTotal(section.getTransitTime());
        sectionDTO.setDuration(duration);

        var cost = new ReportEntryDTO();
        cost.setTotal(section.getRate());
        sectionDTO.setCost(cost);

        return sectionDTO;
    }

    private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight) {
        Map<String, ReportEntryDTO> risk = new HashMap<>();

        var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add);

        var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        var worstValue = destination.stream().map(CalculationJobDestination::getTotalRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        var bestValue = destination.stream().map(CalculationJobDestination::getTotalChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);

        var totalValue = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        //var totalValue = weightedTotalCost.totalCost.divide(annualAmount, 2,  RoundingMode.HALF_UP);

        totalValue = totalValue.add(airfreightValue.multiply(BigDecimal.valueOf(includeAirfreight ? 1 : 0)));

        ReportEntryDTO total = new ReportEntryDTO();
        total.setTotal(totalValue);
        total.setPercentage(totalValue.divide(totalValue, 4, RoundingMode.HALF_UP));
        risk.put("mek_b", total);

        ReportEntryDTO worst = new ReportEntryDTO();
        worst.setTotal(worstValue);
        worst.setPercentage(worstValue.divide(totalValue, 4, RoundingMode.HALF_UP));
        risk.put("risk_scenario", worst);

        ReportEntryDTO best = new ReportEntryDTO();
        best.setTotal(bestValue);
        best.setPercentage(bestValue.divide(totalValue, 4, RoundingMode.HALF_UP));
        risk.put("opportunity_scenario", best);

        return risk;
    }

    private Map<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight) {
        Map<String, ReportEntryDTO> cost = new HashMap<>();

        var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add);

        var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);

        var materialValue = destination.stream().map(CalculationJobDestination::getMaterialCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(destination.size()), 4, RoundingMode.HALF_UP);
        var fcaFeesValues = destination.stream().map(CalculationJobDestination::getFcaCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        var repackingValues = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualRepackingCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        var handlingValues = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualHandlingCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        var storageValues = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualStorageCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        var capitalValues = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualCapitalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        var disposalValues = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualDisposalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
        var customValues = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualCustomCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);

        var noTransportSplit = true; //= weightedTotalCost.totalPreRunCost.compareTo(BigDecimal.ZERO) == 0 && weightedTotalCost.totalMainRunCost.compareTo(BigDecimal.ZERO) == 0 && weightedTotalCost.totalPostRunCost.compareTo(BigDecimal.ZERO) == 0;


        var preRunValues = weightedTotalCost.totalPreRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP);
        var mainRunValues = weightedTotalCost.totalMainRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP);
        var postRunValues = weightedTotalCost.totalPostRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP);

        var d2dValues = weightedTotalCost.totalD2D.divide(annualAmount, 4, RoundingMode.HALF_UP);
        var transportValues = weightedTotalCost.totalCost.divide(annualAmount, 4, RoundingMode.HALF_UP);


        var totalValue = annualAmount.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);

        if (includeAirfreight) {
            totalValue = totalValue.add(airfreightValue);
        }

        ReportEntryDTO total = new ReportEntryDTO();
        total.setTotal(totalValue);
        total.setPercentage(BigDecimal.valueOf(1));
        cost.put("total", total);

        if (includeAirfreight) {
            ReportEntryDTO airfreight = new ReportEntryDTO();
            airfreight.setTotal(airfreightValue);
            airfreight.setPercentage(airfreightValue.divide(totalValue, 4, RoundingMode.HALF_UP));
            cost.put("air_freight_cost", airfreight);
        }

        if (!noTransportSplit) {
            ReportEntryDTO preRun = new ReportEntryDTO();
            preRun.setTotal(preRunValues);
            preRun.setPercentage(preRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
            cost.put("pre_run", preRun);
        }
        if (!noTransportSplit) {
            ReportEntryDTO mainRun = new ReportEntryDTO();
            mainRun.setTotal(mainRunValues);
            mainRun.setPercentage(mainRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
            cost.put("main_run", mainRun);
        }
        if (!noTransportSplit) {
            ReportEntryDTO postRun = new ReportEntryDTO();
            postRun.setTotal(postRunValues);
            postRun.setPercentage(postRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
            cost.put("post_run", postRun);
        }
        if (d2dValues.compareTo(BigDecimal.ZERO) != 0) {
            ReportEntryDTO d2d = new ReportEntryDTO();
            d2d.setTotal(d2dValues);
            d2d.setPercentage(d2dValues.divide(totalValue, 4, RoundingMode.HALF_UP));
            cost.put("d2d", d2d);
        } else if (noTransportSplit) {
            ReportEntryDTO transport = new ReportEntryDTO();
            transport.setTotal(transportValues);
            transport.setPercentage(transportValues.divide(totalValue, 4, RoundingMode.HALF_UP));
            cost.put("transport", transport);
        }

        ReportEntryDTO material = new ReportEntryDTO();
        material.setTotal(materialValue);
        material.setPercentage(materialValue.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("mek_a", material);

        ReportEntryDTO custom = new ReportEntryDTO();
        custom.setTotal(customValues);
        custom.setPercentage(customValues.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("custom", custom);

        ReportEntryDTO fcaFees = new ReportEntryDTO();
        fcaFees.setTotal(fcaFeesValues);
        fcaFees.setPercentage(fcaFeesValues.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("fca_fees", fcaFees);

        ReportEntryDTO repacking = new ReportEntryDTO();
        repacking.setTotal(repackingValues);
        repacking.setPercentage(repackingValues.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("repacking", repacking);

        ReportEntryDTO handling = new ReportEntryDTO();
        handling.setTotal(handlingValues);
        handling.setPercentage(handlingValues.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("handling", handling);

        ReportEntryDTO storage = new ReportEntryDTO();
        storage.setTotal(storageValues);
        storage.setPercentage(storageValues.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("storage", storage);

        ReportEntryDTO capital = new ReportEntryDTO();
        capital.setTotal(capitalValues);
        capital.setPercentage(capitalValues.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("capital", capital);

        ReportEntryDTO disposal = new ReportEntryDTO();
        disposal.setTotal(disposalValues);
        disposal.setPercentage(disposalValues.divide(totalValue, 4, RoundingMode.HALF_UP));
        cost.put("disposal", disposal);

        return cost;
    }

    private record TimePeriod(LocalDateTime startDate, LocalDateTime endDate) {
    }

    private record WeightedTotalCosts(BigDecimal totalPreRunCost, BigDecimal totalMainRunCost,
                                      BigDecimal totalPostRunCost, BigDecimal totalD2D, BigDecimal totalCost) {
    }
}
