package de.avatic.lcc.service.precalculation;

import de.avatic.lcc.dto.generic.ContainerType;
import de.avatic.lcc.dto.generic.RateType;
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.Route;
import de.avatic.lcc.model.db.premises.route.RouteSection;
import de.avatic.lcc.model.db.properties.PropertySet;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.db.rates.ValidityPeriod;
import de.avatic.lcc.model.db.rates.ValidityPeriodState;
import de.avatic.lcc.model.db.utils.WeightUnit;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.premise.*;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.repositories.properties.PropertySetRepository;
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
import de.avatic.lcc.repositories.rates.ValidityPeriodRepository;
import de.avatic.lcc.service.access.PropertyService;
import de.avatic.lcc.service.api.CustomApiService;
import de.avatic.lcc.service.api.TaxationResolverService;
import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
import de.avatic.lcc.util.exception.internalerror.PremiseValidationError;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

@Service
public class PreCalculationCheckService {

    private static final double DIMENSION_TOLERANCE = 0.02;

    private final PremiseRepository premiseRepository;
    private final CustomApiService customApiService;
    private final DestinationRepository destinationRepository;
    private final RouteRepository routeRepository;
    private final NodeRepository nodeRepository;
    private final DimensionTransformer dimensionTransformer;

    private final PropertyService propertyService;
    private final RouteSectionRepository routeSectionRepository;
    private final RouteNodeRepository routeNodeRepository;
    private final MatrixRateRepository matrixRateRepository;
    private final ContainerRateRepository containerRateRepository;
    private final ValidityPeriodRepository validityPeriodRepository;
    private final PropertySetRepository propertySetRepository;
    private final PropertyRepository propertyRepository;
    private final TaxationResolverService eUTaxationResolverService;

    public PreCalculationCheckService(PremiseRepository premiseRepository, CustomApiService customApiService, DestinationRepository destinationRepository, RouteRepository routeRepository, NodeRepository nodeRepository, DimensionTransformer dimensionTransformer, PropertyService propertyService, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, MatrixRateRepository matrixRateRepository, ContainerRateRepository containerRateRepository, ValidityPeriodRepository validityPeriodRepository, PropertySetRepository propertySetRepository, PropertyRepository propertyRepository, TaxationResolverService eUTaxationResolverService) {
        this.premiseRepository = premiseRepository;
        this.customApiService = customApiService;
        this.destinationRepository = destinationRepository;
        this.routeRepository = routeRepository;
        this.nodeRepository = nodeRepository;
        this.dimensionTransformer = dimensionTransformer;

        this.propertyService = propertyService;
        this.routeSectionRepository = routeSectionRepository;
        this.routeNodeRepository = routeNodeRepository;
        this.matrixRateRepository = matrixRateRepository;
        this.containerRateRepository = containerRateRepository;
        this.validityPeriodRepository = validityPeriodRepository;
        this.propertySetRepository = propertySetRepository;
        this.propertyRepository = propertyRepository;
        this.eUTaxationResolverService = eUTaxationResolverService;
    }

    @Async("calculationExecutor")
    public CompletableFuture<Void> doPrecheck(Integer premiseId, Integer setId, Integer periodId) {
        var premise = premiseRepository.getPremiseById(premiseId).orElseThrow();

        supplierCheck(premise);

        materialCheck(premise);

        packagingCheck(premise);

        priceCheck(premise);

        var destinations = destinationRepository.getByPremiseId(premiseId);

        if (destinations == null || destinations.isEmpty()) {
            throw new PremiseValidationError("No destination defined yet, please add at least one destination.");
        }


        for (Destination destination : destinations) {

            var node = nodeRepository.getByDestinationId(destination.getId()).orElseThrow();

            destinationCheck(destination, node);

            var routes = routeRepository.getByDestinationId(destination.getId());


            if (routes.isEmpty() && destination.getD2d() == false)
                throw new PremiseValidationError("No standard route available for destination " + node.getName() + ". Please use individual rate or contact your administrator.");

            if (routes.stream().noneMatch(Route::getSelected) && destination.getD2d() == false)
                throw new PremiseValidationError("No route selected for destination " + node.getName());

            if (destination.getD2d() && (destination.getRateD2d() == null || destination.getRateD2d().compareTo(BigDecimal.ZERO) == 0)) {
                throw new PremiseValidationError("Door-2-door rate not entered or zero.");
            }

            if (destination.getD2d() && (destination.getLeadTimeD2d() == null || destination.getLeadTimeD2d() == 0)) {
                throw new PremiseValidationError("Door-2-door lead time not entered or zero.");
            }

            var period = validityPeriodRepository.getById(periodId);
            var set = propertySetRepository.getById(setId);

            periodCheck(period, set);

            routes.stream().filter(Route::getSelected).findAny().ifPresent(r -> {
                var sections = routeSectionRepository.getByRouteId(r.getId());
                routeCheck(sections, period);
            });


        }

        return CompletableFuture.completedFuture(null);
    }

    private void periodCheck(ValidityPeriod period, PropertySet set) {


        if (set == null)
            throw new PremiseValidationError("There are no system properties for the given date. Please contact your administrator.");

        if (period == null)
            throw new PremiseValidationError("There are no rates for the given date. Please contact your administrator.");

        if (ValidityPeriodState.VALID != period.getState() && ValidityPeriodState.EXPIRED != period.getState())
            throw new PremiseValidationError("There are no valid rates for the given date. Please contact your administrator.");

        if (ValidityPeriodState.VALID != set.getState() && ValidityPeriodState.EXPIRED != period.getState())
            throw new PremiseValidationError("There are no valid system properties for the given date. Please contact your administrator.");

        //TODO: sicherstellen, dass die valid days für den zeitpunkt galten zu dem die valid period galt (wenn rückwirkend gerechnet wird)
        var validDays = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.VALID_DAYS, set.getId());
        var renewals = period.getRenewals();

        if (validDays.isEmpty())
            throw new PremiseValidationError("There are no valid days property. Please contact your administrator");

        var validDaysInt = Integer.parseInt(validDays.get().getCurrentValue());

        if (!period.getStartDate().plusDays((((long) validDaysInt * renewals) + validDaysInt)).isAfter(LocalDateTime.now()))
            throw new PremiseValidationError("There are no valid rates for the given date. Please contact your administrator.");

    }

    private void routeCheck(List<RouteSection> sections, ValidityPeriod period) {

        sections.forEach(section -> {
            var fromRouteNode = routeNodeRepository.getFromNodeBySectionId(section.getId());
            var toRouteNode = routeNodeRepository.getToNodeBySectionId(section.getId());

            if (fromRouteNode.isEmpty() || toRouteNode.isEmpty())
                throw new PremiseValidationError("Error in route. Please contact your administrator.");

            if (RateType.MATRIX == section.getRateType()) {
                var rate = matrixRateRepository.getByCountryIds(fromRouteNode.get().getCountryId(), toRouteNode.get().getCountryId(), period.getId());


                if (rate.isEmpty())
                    throw new PremiseValidationError("The transport rates for the period " + period.getStartDate().format(DateTimeFormatter.ISO_DATE) + " to " + period.getEndDate().format(DateTimeFormatter.ISO_DATE) + " are insufficient to perform the calculation for route segment" + fromRouteNode.get().getExternalMappingId() + " to " + toRouteNode.get().getExternalMappingId());

            }

            if (RateType.CONTAINER == section.getRateType()) {
                var rate = containerRateRepository.findRoute(fromRouteNode.get().getNodeId(), toRouteNode.get().getNodeId(), period.getId(), section.getTransportType());

                if (rate.isEmpty())
                    throw new PremiseValidationError("The transport rates for the period " + period.getStartDate().format(DateTimeFormatter.ISO_DATE) + " to " + period.getEndDate().format(DateTimeFormatter.ISO_DATE) + " are insufficient to perform the calculation for route segment" + fromRouteNode.get().getExternalMappingId() + " to " + toRouteNode.get().getExternalMappingId());

            }

        });

    }

    private void destinationCheck(Destination destination, Node node) {

        if (destination.getAnnualAmount() == null || destination.getAnnualAmount() == 0)
            throw new PremiseValidationError("In destination " + node.getName() + ": annual quantity must be greater than zero.");

        if (destination.getD2d() == null)
            throw new PremiseValidationError("In destination " + node.getName() + ": Door-2-Door flag is missing. Please contact administrator.");

        if (destination.getD2d() == true) {
            if (destination.getRateD2d() == null || destination.getRateD2d().compareTo(BigDecimal.ZERO) == 0) {
                throw new PremiseValidationError("In destination " + node.getName() + ": Door-2-door rate not entered or zero.");
            }

            if (destination.getLeadTimeD2d() == null || destination.getLeadTimeD2d() == 0) {
                throw new PremiseValidationError("In destination " + node.getName() + ": Door-2-Door lead time not entered or zero.");
            }
        }

        if (destination.getCountryId() == null || destination.getCountryId() == 0) {
            throw new PremiseValidationError("In destination " + node.getName() + ": destination country ID not set. Please contact your administrator.");
        }

        if (destination.getGeoLat() == null || destination.getGeoLng() == null) {
            throw new PremiseValidationError("In destination " + node.getName() + ": destination geo location not set. Please contact your administrator.");
        }

        if (destination.getDisposalCost() != null && destination.getDisposalCost().compareTo(BigDecimal.ZERO) < 0) {
            throw new PremiseValidationError("In destination " + node.getName() + ": disposal costs are not set or entered value is less than zero.");
        }

        if (destination.getHandlingCost() != null && destination.getHandlingCost().compareTo(BigDecimal.ZERO) < 0) {
            throw new PremiseValidationError("In destination " + node.getName() + ": handling costs are not set or entered value is less than zero.");
        }

        if (destination.getRepackingCost() != null && destination.getRepackingCost().compareTo(BigDecimal.ZERO) < 0) {
            throw new PremiseValidationError("In destination " + node.getName() + ": repackaging costs are not set or entered value is less than zero.");
        }

    }

    private void supplierCheck(Premise premise) {

        if (premise.getSupplierNodeId() == null && premise.getUserSupplierNodeId() == null)
            throw new PremiseValidationError("Supplier node not set. Please contact administrator");

    }

    private void priceCheck(Premise premise) {

        if (premise.getMaterialCost() == null || premise.getMaterialCost().compareTo(BigDecimal.ZERO) == 0) {
            throw new PremiseValidationError("MEK_A not entered or zero.");
        }

        if (premise.getOverseaShare() == null) {
            throw new PremiseValidationError("Oversea share not entered.");
        }

        if (premise.getFcaEnabled() == null) {
            throw new PremiseValidationError("FCA fee not set. Please contact administrator");
        }

    }

    private void packagingCheck(Premise premise) {

        if (premise.getHuMixable() == null) {
            throw new PremiseValidationError("Mixable not set. Please contact administrator.");
        }

        if (premise.getHuStackable() == null) {
            throw new PremiseValidationError("Stackable not set. Please contact administrator.");
        }

        if (premise.getHuStackable() == false && premise.getHuMixable() == true) {
            throw new PremiseValidationError("Stackable cannot be false, when mixable is true.");
        }

        if (premise.getIndividualHuLength() == null || premise.getIndividualHuLength() == 0) {
            throw new PremiseValidationError("Packaging length not entered or zero.");
        }

        if (premise.getIndividualHuWidth() == null || premise.getIndividualHuWidth() == 0) {
            throw new PremiseValidationError("Packaging width not entered or zero.");
        }

        if (premise.getIndividualHuHeight() == null || premise.getIndividualHuHeight() == 0) {
            throw new PremiseValidationError("Packaging height not entered or zero.");
        }

        if (premise.getIndividualHuWeight() == null || premise.getIndividualHuWeight() == 0) {
            throw new PremiseValidationError("Packaging weight not entered or zero");
        }

        if (premise.getHuUnitCount() == null || premise.getHuUnitCount() == 0) {
            throw new PremiseValidationError("Packaging unit count not entered or zero.");
        }

        if (premise.getHuDisplayedWeightUnit() == null) {
            throw new PremiseValidationError("Packaging weight unit not set. Please contact administrator.");
        }

        if (premise.getHuDisplayedDimensionUnit() == null) {
            throw new PremiseValidationError("Packaging dimension unit not set. Please contact administrator.");
        }

        var hu = dimensionTransformer.toDimensionEntity(premise).withTolerance(DIMENSION_TOLERANCE);

        Optional<Integer> teuLoad = propertyService.getProperty(SystemPropertyMappingId.TEU_LOAD);
        Optional<Integer> feuLoad = propertyService.getProperty(SystemPropertyMappingId.TEU_LOAD);

        if (teuLoad.isEmpty() || feuLoad.isEmpty())
            throw new PremiseValidationError("System properties not properly configured. Please contact your administrator.");

        if (WeightUnit.KG.convertFromG(hu.getWeight()) > teuLoad.get() && hu.getWeight() > feuLoad.get())
            throw new PremiseValidationError("HU weight exceeds maximum container weight load (" + Math.max(teuLoad.get(), feuLoad.get()) + " kg). Review entered weight and selected weight unit [g, kg, t].");

        var teuFitsXY = (hu.getLength() < ContainerType.TEU.getLength() && hu.getWidth() < ContainerType.TEU.getWidth());
        var teuFitsYX = (hu.getWidth() < ContainerType.TEU.getLength() && hu.getLength() < ContainerType.TEU.getWidth());

        if (!teuFitsYX && !teuFitsXY) {
            throw new PremiseValidationError("HU dimensions too large. Review entered length, width, height and selected dimension unit [mm, cm, m].");
        }


        if ((hu.getLength() * hu.getWidth()) < 20000) {
            throw new PremiseValidationError("HU dimensions too small. Review entered length, width, height and selected dimension unit [mm, cm, m].");
        }

    }

    private void materialCheck(Premise premise) {

//        if (premise.getTariffUnlocked()) {
//            if (premise.getHsCode() == null || premise.getHsCode().length() < 10)
//                throw new PremiseValidationError("Invalid HS code (10 digits expected).");
//
//            var isDeclarable = eUTaxationResolverService.validate(premise.getHsCode());
//
//            if (!isDeclarable) throw new PremiseValidationError("Invalid HS code (not declarable).");
//        }

        if (premise.getTariffRate() == null) {
            throw new PremiseValidationError("Tariff rate not entered.");
        }
    }

}
