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

import de.avatic.lcc.model.calculation.ContainerCalculationResult;
import de.avatic.lcc.dto.generic.ContainerType;
import de.avatic.lcc.dto.generic.RateType;
import de.avatic.lcc.dto.generic.TransportType;
import de.avatic.lcc.model.db.calculations.CalculationJobRouteSection;
import de.avatic.lcc.model.db.nodes.Location;
import de.avatic.lcc.model.db.nodes.Node;
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.RouteNode;
import de.avatic.lcc.model.db.premises.route.RouteSection;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.db.rates.ContainerRate;
import de.avatic.lcc.model.db.rates.MatrixRate;
import de.avatic.lcc.model.db.utils.DimensionUnit;
import de.avatic.lcc.model.db.utils.WeightUnit;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.premise.RouteNodeRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
import de.avatic.lcc.repositories.users.UserNodeRepository;
import de.avatic.lcc.service.calculation.DistanceService;
import org.springframework.stereotype.Service;

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

@Service
public class RouteSectionCostCalculationService {

    private final ContainerRateRepository containerRateRepository;
    private final MatrixRateRepository matrixRateRepository;
    private final RouteNodeRepository routeNodeRepository;
    private final DistanceService distanceService;
    private final PropertyRepository propertyRepository;
    private final ChangeRiskFactorCalculationService changeRiskFactorCalculationService;
    private final NodeRepository nodeRepository;
    private final UserNodeRepository userNodeRepository;

    public RouteSectionCostCalculationService(ContainerRateRepository containerRateRepository, MatrixRateRepository matrixRateRepository, RouteNodeRepository routeNodeRepository, DistanceService distanceService, PropertyRepository propertyRepository, ChangeRiskFactorCalculationService changeRiskFactorCalculationService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository) {
        this.containerRateRepository = containerRateRepository;
        this.matrixRateRepository = matrixRateRepository;
        this.routeNodeRepository = routeNodeRepository;
        this.distanceService = distanceService;
        this.propertyRepository = propertyRepository;
        this.changeRiskFactorCalculationService = changeRiskFactorCalculationService;
        this.nodeRepository = nodeRepository;
        this.userNodeRepository = userNodeRepository;
    }

    public CalculationJobRouteSection doD2dCalculation(Integer setId, Integer periodId, Premise premise, Destination destination, ContainerCalculationResult containerCalculation) {
        CalculationJobRouteSection result = new CalculationJobRouteSection();

        // Set route metadata
        result.setMainRun(false);
        result.setPostRun(false);
        result.setPreRun(false);
        result.setTransportType(null);
        result.setRateType(RateType.D2D);

        // Set premise metadata
        result.setStacked(premise.getHuStackable());
        result.setUnmixedPrice(premise.getHuMixable());

        // Get nodes and distance
        Node fromNode = nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();
        Node toNode = nodeRepository.getById(destination.getDestinationNodeId()).orElseThrow();

        double distance = distanceService.getDistance(fromNode, toNode, false);
        result.setDistance(BigDecimal.valueOf(distance));

        // Get rate and transit time
        BigDecimal rate = destination.getRateD2d();
        int transitTime = destination.getLeadTimeD2d();

        result.setRate(rate);
        result.setTransitTime(transitTime);

        // Calculate price and annual cost
        BigDecimal utilization = getUtilization(setId, RateType.CONTAINER); /* D2D is always 40ft container */
        double annualVolume = destination.getAnnualAmount() * containerCalculation.getHu().getVolume(DimensionUnit.M);

        PriceCalculationResult prices = calculatePrices(
                premise.getHuMixable(),
                rate,
                containerCalculation.isWeightExceeded(),
                ContainerType.FEU,
                containerCalculation.getMaxContainerWeight(),
                BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
                BigDecimal.valueOf(containerCalculation.getHuUtilizationByWeight()),
                utilization);

        result.setCbmPrice(!containerCalculation.isWeightExceeded());
        result.setWeightPrice(containerCalculation.isWeightExceeded());
        result.setCbmPrice(prices.volumePrice);
        result.setWeightPrice(prices.weightPrice);
        result.setUtilization(prices.utilization);

        var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors(setId, periodId);

        BigDecimal annualCost = (containerCalculation.isWeightExceeded() ? prices.weightPrice : prices.volumePrice).multiply(BigDecimal.valueOf(annualVolume));
        BigDecimal annualRiskCost = annualCost.multiply(chanceRiskFactors.getRiskFactor());
        BigDecimal annualChanceCost = annualCost.multiply(chanceRiskFactors.getChanceFactor());

        result.setAnnualRiskCost(annualRiskCost);
        result.setAnnualChanceCost(annualChanceCost);
        result.setAnnualCost(annualCost);

        return result;
    }

    public CalculationJobRouteSection doCalculation(Integer setId, Integer periodId, Premise premise, Destination destination, RouteSection section, ContainerCalculationResult containerCalculation) {
        CalculationJobRouteSection result = new CalculationJobRouteSection();

        // Set route metadata
        result.setMainRun(section.getMainRun());
        result.setPostRun(section.getPostRun());
        result.setPreRun(section.getPreRun());
        result.setTransportType(TransportType.valueOf(section.getTransportType().name()));
        result.setRateType(RateType.valueOf(section.getRateType().name()));

        // Set premise metadata
        result.setStacked(premise.getHuStackable());
        result.setUnmixedPrice(!premise.getHuMixable());

        // Get nodes and distance
        RouteNode fromNode = routeNodeRepository.getById(section.getFromRouteNodeId()).orElseThrow();
        RouteNode toNode = routeNodeRepository.getById(section.getToRouteNodeId()).orElseThrow();
        double distance = getDistance(fromNode, toNode);
        result.setDistance(BigDecimal.valueOf(distance));

        // Get rate and transit time
        BigDecimal rate;
        int transitTime;

        if (RateType.CONTAINER == section.getRateType()) {
            ContainerRate containerRate = findContainerRate(section, fromNode, toNode, periodId);
            rate = getContainerRate(containerRate, containerCalculation.getContainerType());
            transitTime = containerRate.getLeadTime();
        } else if (RateType.MATRIX == section.getRateType()) {
            MatrixRate matrixRate = findMatrixRate(fromNode, toNode, periodId);
            rate = matrixRate.getRate().multiply(BigDecimal.valueOf(distance));
            transitTime = 3; // Default transit time for matrix rate
        } else if (RateType.NEAR_BY == section.getRateType()) {
            rate = BigDecimal.ZERO;
            transitTime = 0;
        } else {
            throw new IllegalArgumentException("Unsupported rate type: " + section.getRateType());
        }

        result.setRate(rate);
        result.setTransitTime(transitTime);

        // Calculate price and annual cost
        BigDecimal huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(containerCalculation.getHu().getContentUnitCount()), 2, RoundingMode.HALF_UP);
        BigDecimal utilization = getUtilization(setId, section.getRateType());
        BigDecimal annualVolume = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getVolume(DimensionUnit.M)));
        BigDecimal annualWeight = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getWeight(WeightUnit.KG)));

        PriceCalculationResult prices = calculatePrices(
                premise.getHuMixable(),
                rate,
                containerCalculation.isWeightExceeded(),
                containerCalculation.getContainerType(),
                containerCalculation.getMaxContainerWeight(),
                BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
                BigDecimal.valueOf(containerCalculation.getTotalUtilizationByWeight()),
                utilization);

        result.setCbmPrice(!containerCalculation.isWeightExceeded());
        result.setWeightPrice(containerCalculation.isWeightExceeded());
        result.setCbmPrice(prices.volumePrice);
        result.setWeightPrice(prices.weightPrice);
        result.setUtilization(!containerCalculation.isWeightExceeded() || !premise.getHuMixable() ? prices.utilization : BigDecimal.valueOf(1.0));

        var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors(setId, periodId);

        var useRiskChange = (RateType.MATRIX != section.getRateType()) && (TransportType.RAIL == section.getTransportType() || TransportType.SEA == section.getTransportType());


        BigDecimal annualCost = (containerCalculation.isWeightExceeded() ? prices.weightPrice.multiply(annualWeight) : prices.volumePrice.multiply(annualVolume));
        BigDecimal annualRiskCost =  useRiskChange ? annualCost.multiply(chanceRiskFactors.getRiskFactor()) : annualCost;
        BigDecimal annualChanceCost = useRiskChange ? annualCost.multiply(chanceRiskFactors.getChanceFactor()): annualCost;

        result.setAnnualRiskCost(annualRiskCost);
        result.setAnnualChanceCost(annualChanceCost);
        result.setAnnualCost(annualCost);

        result.setPremiseRouteSectionId(section.getId());

        return result;
    }

    private PriceCalculationResult calculatePrices(
            boolean huMixable,
            BigDecimal rate,
            boolean weightExceeded,
            ContainerType containerType,
            int maxContainerWeight,
            BigDecimal totalVolumeUtilization,
            BigDecimal totalWeightUtilization,
            BigDecimal propertyUtilization) {


        BigDecimal utilization;
        BigDecimal volumePrice;
        BigDecimal weightPrice;

        BigDecimal cbmRate = rate.divide(BigDecimal.valueOf(containerType.getVolume()), 10, RoundingMode.HALF_UP);
        BigDecimal weightRate = rate.divide(BigDecimal.valueOf(maxContainerWeight), 10, RoundingMode.HALF_UP);

        if (huMixable) {
            volumePrice = cbmRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP);
            weightPrice = weightRate.divide(BigDecimal.valueOf(1), 10, RoundingMode.HALF_UP);
            utilization = weightExceeded ? BigDecimal.ONE : propertyUtilization;
        } else {
            volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP);
            weightPrice = weightRate.divide(totalWeightUtilization, 10, RoundingMode.HALF_UP);
            utilization = weightExceeded ? totalWeightUtilization : totalVolumeUtilization;
        }

        return new PriceCalculationResult(volumePrice, weightPrice, utilization);
    }

    private ContainerRate findContainerRate(RouteSection section, RouteNode fromNode, RouteNode toNode, Integer periodId) {
        return containerRateRepository.findRoute(
                        fromNode.getNodeId(),
                        toNode.getNodeId(),
                        periodId,
                        TransportType.valueOf(section.getTransportType().name()))
                .orElseThrow(() -> new NoSuchElementException(
                        "ContainerRate not found for route: " + fromNode.getName() + "(" + fromNode.getNodeId() + ")" +
                                " to " + toNode.getName() + "(" + toNode.getNodeId() + ")"));
    }

    private MatrixRate findMatrixRate(RouteNode fromNode, RouteNode toNode, Integer periodId) {
        return matrixRateRepository.getByCountryIds(fromNode.getCountryId(), toNode.getCountryId(), periodId)
                .orElseThrow(() -> new NoSuchElementException(
                        "MatrixRate not found for countries: " + fromNode.getCountryId() +
                                " to " + toNode.getCountryId()));
    }

    private BigDecimal getUtilization(Integer setId, RateType rateType) {
        BigDecimal utilization;
        if (rateType == RateType.NEAR_BY) {
            utilization = BigDecimal.valueOf(Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CONTAINER_UTIL, setId).orElseThrow().getCurrentValue()));
        } else if (rateType == RateType.CONTAINER) {
            utilization = BigDecimal.valueOf(Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CONTAINER_UTIL, setId).orElseThrow().getCurrentValue()));
        } else if (rateType == RateType.MATRIX) {
            utilization = BigDecimal.valueOf(Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.TRUCK_UTIL, setId).orElseThrow().getCurrentValue()));
        } else
            throw new IllegalArgumentException("Unknown rate type");

        return utilization;
    }


    private BigDecimal getContainerRate(ContainerRate rate, ContainerType containerType) {

        switch (containerType) {
            case HC -> {
                return rate.getRateHc();
            }
            case FEU -> {
                return rate.getRateFeu();
            }
            case TEU -> {
                return rate.getRateTeu();
            }
        }

        throw new IllegalArgumentException("Unknown container type");
    }

    private double getDistance(RouteNode fromNode, RouteNode toNode) {

        if(fromNode.getOutdated() || toNode.getOutdated()) {
            return distanceService.getDistance(
                    fromNode.getCountryId(), new Location(fromNode.getGeoLng().doubleValue(), fromNode.getGeoLat().doubleValue()),
                    toNode.getCountryId(), new Location(toNode.getGeoLng().doubleValue(), toNode.getGeoLat().doubleValue()), false);
        }

        var optSrcNode = fromNode.getNodeId() == null ? userNodeRepository.getById(fromNode.getUserNodeId()) : nodeRepository.getById(fromNode.getNodeId());
        var optDestNode = toNode.getNodeId() == null ? userNodeRepository.getById(toNode.getUserNodeId()) : nodeRepository.getById(toNode.getNodeId());

        if(optSrcNode.isEmpty() ) {
            throw new NoSuchElementException("Source node not found for route section " + fromNode.getName());
        }
        if(optDestNode.isEmpty() ) {
            throw new NoSuchElementException("Destination node not found for route section" + toNode.getName());
        }

        return distanceService.getDistance(optSrcNode.get(), optDestNode.get(), false);

    }


    /**
     * Simple data class to hold volume and weight price results
     */
    private record PriceCalculationResult(BigDecimal volumePrice, BigDecimal weightPrice, BigDecimal utilization) {
    }

}
