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.RouteNode;
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.MaterialRepository;
import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.premise.*;
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.access.PropertyService;
import de.avatic.lcc.service.transformer.generic.DimensionTransformer;
import de.avatic.lcc.util.exception.internalerror.PremiseValidationError;
import org.springframework.stereotype.Service;

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

@Service
public class PreCalculationCheckService {

    private static final double DIMENSION_TOLERANCE = 0.02;

    private final PremiseRepository premiseRepository;
    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 PropertyRepository propertyRepository;
    private final MaterialRepository materialRepository;
    private final UserNodeRepository userNodeRepository;

    public PreCalculationCheckService(PremiseRepository premiseRepository, DestinationRepository destinationRepository, RouteRepository routeRepository, NodeRepository nodeRepository, DimensionTransformer dimensionTransformer, PropertyService propertyService, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, MatrixRateRepository matrixRateRepository, ContainerRateRepository containerRateRepository, PropertyRepository propertyRepository,  MaterialRepository materialRepository,  UserNodeRepository userNodeRepository) {
        this.premiseRepository = premiseRepository;
        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.propertyRepository = propertyRepository;
        this.materialRepository = materialRepository;
        this.userNodeRepository = userNodeRepository;
    }

    public void doPrecheck(Integer premiseId, Optional<PropertySet> set, Optional<ValidityPeriod> period, boolean showCalcInfo) {
        doPrecheck(premiseId, set, period, LocalDate.now(), showCalcInfo);
    }


    public void doPrecheck(Integer premiseId, Optional<PropertySet> set, Optional<ValidityPeriod> period, LocalDate date, boolean showCalcInfo) {
        var premise = premiseRepository.getPremiseById(premiseId).orElseThrow();

        supplierCheck(premise);

        var material = materialRepository.getById(premise.getMaterialId());
        var supplier = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()) : nodeRepository.getById(premise.getSupplierNodeId());

        if(material.isEmpty() || supplier.isEmpty())
            throw new PremiseValidationError("No material or supplier - please contact your administrator");

        var partNumber = material.get().getPartNumber();
        var supplierName = supplier.get().getName();

        materialCheck(premise, supplierName, partNumber, showCalcInfo);

        packagingCheck(premise, supplierName, partNumber, showCalcInfo);

        priceCheck(premise, supplierName, partNumber, showCalcInfo);

        var destinations = destinationRepository.getByPremiseId(premiseId);

        if (destinations == null || destinations.isEmpty()) {
            throw new PremiseValidationError("Please add at least one destination to continue", supplierName, partNumber, showCalcInfo);
        }

        for (Destination destination : destinations) {

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

            destinationCheck(destination, node, supplierName, partNumber, showCalcInfo);

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


            if (routes.isEmpty() && destination.getD2d() == false)
                throw new PremiseValidationError(String.format("No standard route found for %s - try using an individual rate instead", node.getName()), supplierName, partNumber, showCalcInfo);

            if (routes.stream().noneMatch(Route::getSelected) && destination.getD2d() == false)
                throw new PremiseValidationError(String.format("Please select a route for %s", node.getName()), supplierName, partNumber, showCalcInfo);

            if (destination.getD2d() && (destination.getRateD2d() == null || destination.getRateD2d().compareTo(BigDecimal.ZERO) == 0)) {
                throw new PremiseValidationError("Please enter a door-to-door rate greater than zero", supplierName, partNumber, showCalcInfo);
            }

            if (destination.getD2d() && (destination.getLeadTimeD2d() == null || destination.getLeadTimeD2d() == 0)) {
                throw new PremiseValidationError("Please enter a door-to-door lead time", supplierName, partNumber, showCalcInfo);
            }

            periodCheck(period.orElse(null), set.orElse(null), date, supplierName, partNumber);

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

    private void periodCheck(ValidityPeriod period, PropertySet set, LocalDate calculationDate, String supplierName, String partNumber) {

        if (set == null)
            throw new PremiseValidationError("No system configuration available for this date - please contact your administrator");

        if (period == null)
            throw new PremiseValidationError("No rates available for this date - please contact your administrator");

        if (ValidityPeriodState.VALID != period.getState() && ValidityPeriodState.EXPIRED != period.getState())
            throw new PremiseValidationError("Rates for this date aren't valid yet - please contact your administrator");

        if (ValidityPeriodState.VALID != set.getState() && ValidityPeriodState.EXPIRED != period.getState())
            throw new PremiseValidationError("System properties for this date aren't valid yet - please contact your administrator");


        if (calculationDate == null) {

            var validDays = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.VALID_DAYS, set.getId());
            var renewals = period.getRenewals();

            if (validDays.isEmpty())
                throw new PremiseValidationError("Missing system configuration (VALID_DAYS) - 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("Rates for this date aren't valid yet - please contact your administrator");
        }

    }

    private void routeCheck(List<RouteSection> sections, ValidityPeriod period, LocalDate calculationDate, String supplierName, String partNumber, boolean showCalcInfo) {

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

            if (fromRouteNode.isEmpty() || toRouteNode.isEmpty())
                throw new PremiseValidationError("Route configuration issue - please contact your administrator", supplierName, partNumber, showCalcInfo);

            if (RateType.MATRIX == section.getRateType()) {
                var rate = matrixRateRepository.getByCountryIds(fromRouteNode.get().getCountryId(), toRouteNode.get().getCountryId(), period.getId());
                constructRouteSectionError(calculationDate, fromRouteNode.get(), toRouteNode.get(), rate.isEmpty(), supplierName, partNumber, showCalcInfo);
            }

            if (RateType.CONTAINER == section.getRateType()) {
                var rate = containerRateRepository.findRoute(fromRouteNode.get().getNodeId(), toRouteNode.get().getNodeId(), period.getId(), section.getTransportType());
                constructRouteSectionError(calculationDate, fromRouteNode.get(), toRouteNode.get(), rate.isEmpty(), supplierName, partNumber, showCalcInfo);
            }
        });
    }

    private void constructRouteSectionError(LocalDate calculationDate, RouteNode fromRouteNode, RouteNode toRouteNode, boolean empty, String supplierName, String partNumber, boolean showCalcInfo) {
        if (empty) {
            var dateStr = calculationDate == null ? "" : String.format("on %s", calculationDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")));
            var errStr = String.format("Transport rate missing for %s to %s %s - please contact your administrator",
                    fromRouteNode.getExternalMappingId(),
                    toRouteNode.getExternalMappingId(),
                    dateStr);
            throw new PremiseValidationError(errStr, supplierName, partNumber, showCalcInfo);
        }
    }

    private void destinationCheck(Destination destination, Node node, String supplierName, String partNumber, boolean showCalcInfo) {

        if (destination.getAnnualAmount() == null || destination.getAnnualAmount() == 0)
            throw new PremiseValidationError(String.format("Annual quantity for %s must be greater than zero", node.getName()), supplierName, partNumber, showCalcInfo);

        if (destination.getD2d() == null)
            throw new PremiseValidationError(String.format("Something's missing for %s - please contact your administrator", node.getName()), supplierName, partNumber, showCalcInfo);

        if (destination.getD2d() == true) {
            if (destination.getRateD2d() == null || destination.getRateD2d().compareTo(BigDecimal.ZERO) == 0) {
                throw new PremiseValidationError(String.format("Door-to-door rate for %s needs to be greater than zero", node.getName()), supplierName, partNumber, showCalcInfo);
            }

            if (destination.getLeadTimeD2d() == null || destination.getLeadTimeD2d() == 0) {
                throw new PremiseValidationError(String.format("Please set a lead time for door-to-door delivery to %s", node.getName()), supplierName, partNumber, showCalcInfo);
            }
        }

        if (destination.getCountryId() == null || destination.getCountryId() == 0) {
            throw new PremiseValidationError(String.format("Configuration issue with %s - please contact your administrator", node.getName()), supplierName, partNumber, showCalcInfo);
        }

        if (destination.getGeoLat() == null || destination.getGeoLng() == null) {
            throw new PremiseValidationError(String.format("Location data missing for %s - please contact your administrator", node.getName()), supplierName, partNumber, showCalcInfo);
        }

        if (destination.getDisposalCost() != null && destination.getDisposalCost().compareTo(BigDecimal.ZERO) < 0) {
            throw new PremiseValidationError(String.format("Disposal costs for %s can't be negative", node.getName()), supplierName, partNumber, showCalcInfo);
        }

        if (destination.getHandlingCost() != null && destination.getHandlingCost().compareTo(BigDecimal.ZERO) < 0) {
            throw new PremiseValidationError(String.format("Handling costs for %s can't be negative", node.getName()), supplierName, partNumber, showCalcInfo);
        }

        if (destination.getRepackingCost() != null && destination.getRepackingCost().compareTo(BigDecimal.ZERO) < 0) {
            throw new PremiseValidationError(String.format("Repackaging costs for %s can't be negative", node.getName()), supplierName, partNumber, showCalcInfo);
        }

    }

    private void supplierCheck(Premise premise) {

        if (premise.getSupplierNodeId() == null && premise.getUserSupplierNodeId() == null)
            throw new PremiseValidationError("Supplier information is missing - please contact your administrator");

    }

    private void priceCheck(Premise premise, String supplierName, String partNumber, boolean showCalcInfo) {

        if (premise.getMaterialCost() == null || premise.getMaterialCost().compareTo(BigDecimal.ZERO) == 0) {
            throw new PremiseValidationError("Please enter a material cost (MEK_A) greater than zero", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getOverseaShare() == null) {
            throw new PremiseValidationError("Please enter the overseas share", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getFcaEnabled() == null) {
            throw new PremiseValidationError("FCA configuration missing - please contact your administrator", supplierName, partNumber, showCalcInfo);
        }

    }

    private void packagingCheck(Premise premise, String supplierName, String partNumber, boolean showCalcInfo) {

        if (premise.getHuMixable() == null) {
            throw new PremiseValidationError("Packaging configuration incomplete - please contact your administrator", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getHuStackable() == null) {
            throw new PremiseValidationError("Packaging configuration incomplete - please contact your administrator", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getHuStackable() == false && premise.getHuMixable() == true) {
            throw new PremiseValidationError("If packaging is mixable, it must also be stackable", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getIndividualHuLength() == null || premise.getIndividualHuLength() == 0) {
            throw new PremiseValidationError("Please enter packaging length", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getIndividualHuWidth() == null || premise.getIndividualHuWidth() == 0) {
            throw new PremiseValidationError("Please enter packaging width", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getIndividualHuHeight() == null || premise.getIndividualHuHeight() == 0) {
            throw new PremiseValidationError("Please enter packaging height", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getIndividualHuWeight() == null || premise.getIndividualHuWeight() == 0) {
            throw new PremiseValidationError("Please enter packaging weight", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getHuUnitCount() == null || premise.getHuUnitCount() == 0) {
            throw new PremiseValidationError("Please enter the number of units per package", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getHuDisplayedWeightUnit() == null) {
            throw new PremiseValidationError("Weight unit configuration missing - please contact your administrator", supplierName, partNumber, showCalcInfo);
        }

        if (premise.getHuDisplayedDimensionUnit() == null) {
            throw new PremiseValidationError("Dimension unit configuration missing - please contact your administrator", supplierName, partNumber, showCalcInfo);
        }

        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 configuration incomplete - please contact your administrator", supplierName, partNumber, showCalcInfo);

        if (WeightUnit.KG.convertFromG(hu.getWeight()) > teuLoad.get() && hu.getWeight() > feuLoad.get())
            throw new PremiseValidationError(String.format("Package weight exceeds %d kg - please check your weight and unit", Math.max(teuLoad.get(), feuLoad.get())), supplierName, partNumber, showCalcInfo);

        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("Package dimensions are too large - please check your measurements and unit", supplierName, partNumber, showCalcInfo);
        }


        if ((hu.getLength() * hu.getWidth()) < 20000) {
            throw new PremiseValidationError("Package dimensions are too small - please check your measurements and unit", supplierName, partNumber, showCalcInfo);
        }

    }

    private void materialCheck(Premise premise, String supplierName, String partNumber, boolean showCalcInfo) {

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

        if (premise.getTariffRate() == null) {
            throw new PremiseValidationError("Please enter a tariff rate", supplierName, partNumber, showCalcInfo);
        }
    }

}