package de.avatic.lcc.service.api;

import de.avatic.lcc.dto.custom.CustomDTO;
import de.avatic.lcc.dto.custom.CustomMeasureDTO;
import de.avatic.lcc.dto.generic.PropertyDTO;
import de.avatic.lcc.model.db.country.Country;
import de.avatic.lcc.model.db.materials.Material;
import de.avatic.lcc.model.db.properties.CountryPropertyMappingId;
import de.avatic.lcc.model.db.properties.CustomUnionType;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.eutaxation.MeasureType;
import de.avatic.lcc.repositories.country.CountryPropertyRepository;
import de.avatic.lcc.repositories.country.CountryRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.service.calculation.NomenclatureService;
import de.avatic.lcc.util.exception.base.InternalErrorException;
import eu.europa.ec.taxation.taric.client.GoodsMeasForWsResponse;
import eu.europa.ec.taxation.taric.client.GoodsMeasuresForWsResponse;
import jakarta.annotation.Nullable;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class TaxationResolverService {

    private final CountryRepository countryRepository;
    private final EUTaxationApiService eUTaxationApiService;
    private final PropertyRepository propertyRepository;
    private final ZolltarifnummernApiService zolltarifnummernApiService;
    private final NomenclatureService nomenclatureService;
    private final CountryPropertyRepository countryPropertyRepository;

    public TaxationResolverService(CountryRepository countryRepository, EUTaxationApiService eUTaxationApiService, PropertyRepository propertyRepository, ZolltarifnummernApiService zolltarifnummernApiService, NomenclatureService nomenclatureService, CountryPropertyRepository countryPropertyRepository) {
        this.countryRepository = countryRepository;
        this.eUTaxationApiService = eUTaxationApiService;
        this.propertyRepository = propertyRepository;
        this.zolltarifnummernApiService = zolltarifnummernApiService;
        this.nomenclatureService = nomenclatureService;
        this.countryPropertyRepository = countryPropertyRepository;
    }


    private Map<TaxationResolverRequest, TaxationResolverApiResponse> doRequests(List<TaxationResolverRequest> requests) {

        /* country id -> union type */
        Map<Integer, CustomUnionType> union = requests.stream()
                .map(TaxationResolverRequest::countryId)
                .distinct()
                .collect(Collectors.toMap(
                        countryId -> countryId,
                        countryId -> countryPropertyRepository
                                .getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, countryId)
                                .map(p -> CustomUnionType.valueOf(p.getCurrentValue()))
                                .orElseThrow()
                ));


        var byCustomUnion = requests.stream().collect(Collectors.groupingBy(r -> (union.get(r.countryId()))));

        /* split by incomplete hs codes and complete hs codes */
        var filteredRequests = byCustomUnion.getOrDefault(CustomUnionType.NONE, Collections.emptyList()).stream().collect(Collectors.partitioningBy(r -> r.material().getHsCode() != null && r.material().getHsCode().length() < 10));

        var joined = Stream.concat(
                filteredRequests.get(false).stream()
                        .filter(r -> r.material().getHsCode() != null)
                        .map(r -> new TaxationResolverSingleRequest(r.material().getHsCode(), r.countryId(), r)),
                resolveIncompleteHsCodesIntern(filteredRequests.get(true)).stream());

        var singleResponses = doSingleRequests(joined.toList());

        return Stream.of(
                byCustomUnion.getOrDefault(CustomUnionType.NONE,Collections.emptyList()).stream().collect(Collectors.toMap(
                        r -> r,
                        r -> new TaxationResolverApiResponse(
                                r.material(),
                                true,
                                singleResponses.keySet().stream().filter(k -> k.origin.equals(r)).map(singleResponses::get).toList()))),

                byCustomUnion.getOrDefault(CustomUnionType.EU,Collections.emptyList()).stream().collect(Collectors.toMap(
                        r -> r,
                        r -> new TaxationResolverApiResponse(
                                r.material(),
                                false,
                                null))))

                .flatMap(map -> map.entrySet().stream()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,(r1,_) -> r1));

    }

    private List<TaxationResolverSingleRequest> resolveIncompleteHsCodesIntern(List<TaxationResolverRequest> request) {
        return request.stream().flatMap(r -> nomenclatureService.getNomenclature(r.material().getHsCode()).stream().map(hsCode -> new TaxationResolverSingleRequest(hsCode, r.countryId(), r))).toList();
    }

    private List<TaxationResolverSingleRequest> resolveIncompleteHsCodes(List<TaxationResolverRequest> request) {

        var futures = request.stream()
                .collect(Collectors.toMap(
                        r -> r,
                        r -> zolltarifnummernApiService.getDeclarableHsCodes(r.material().getHsCode()))
                );

        CompletableFuture.allOf(futures.values().toArray(new CompletableFuture[0])).join();

        return futures.keySet().stream().flatMap(k -> futures.get(k).join().stream().map(resp -> new TaxationResolverSingleRequest(resp, k.countryId(), k))).toList();
    }


    private Map<TaxationResolverSingleRequest, GoodsMeasForWsResponse> doSingleRequests(List<TaxationResolverSingleRequest> requests) {

        Map<Integer, Country> countries = requests.stream()
                .map(TaxationResolverSingleRequest::countryId)
                .distinct()
                .collect(Collectors.toMap(
                        countryId -> countryId,
                        countryId -> countryRepository.getById(countryId).orElseThrow()
                ));


        Map<TaxationResolverSingleRequest, CompletableFuture<GoodsMeasForWsResponse>> futureMap =
                requests.stream()
                        .collect(Collectors.toMap(
                                r -> r,
                                r -> eUTaxationApiService.getGoodsMeasures(
                                        r.hsCode,
                                        countries.get(r.countryId()).getIsoCode().getCode(),
                                        "I"
                                )
                        ));

        CompletableFuture.allOf(futureMap.values().toArray(new CompletableFuture[0])).join();

        return futureMap.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> entry.getValue().join()
                ));
    }

    private Optional<Double> extractDuty(GoodsMeasuresForWsResponse.Measures.Measure measure) {

        var dutyRate = measure.getDutyRate();

        if (dutyRate == null) return Optional.empty();

        if (dutyRate.trim().matches("\\d+\\.\\d+\\s*%")) {
            return Optional.of(Double.parseDouble(dutyRate.trim().replace("%", "").trim()) / 100);
        }

        return Optional.empty();

    }

    private List<GoodsMeasuresForWsResponse.Measures.Measure> filterToNewestMeasuresPerType(List<GoodsMeasuresForWsResponse.Measures.Measure> measures) {
        Map<String, GoodsMeasuresForWsResponse.Measures.Measure> newestByType = new HashMap<>();
        ArrayList<GoodsMeasuresForWsResponse.Measures.Measure> unfiltered = new ArrayList<>();

        for (GoodsMeasuresForWsResponse.Measures.Measure measure : measures) {
            String measureCode = measure.getMeasureType().getMeasureType();

            GoodsMeasuresForWsResponse.Measures.Measure existing = newestByType.get(measureCode);

            var type = MeasureType.fromMeasureCode(measureCode);

            // filter multiples only in C series.
            if (type.isPresent() && type.get().getSeries().equals("C")) {
                if (existing == null ||
                        measure.getValidityStartDate().compare(existing.getValidityStartDate()) > 0) {
                    newestByType.put(measureCode, measure);
                }
            } else unfiltered.add(measure);

        }

        unfiltered.addAll(newestByType.values());
        return unfiltered;
    }

    public boolean validate(String hsCode) {
        try {
            var goodsDescription = eUTaxationApiService.getGoodsDescription(hsCode, "en").join();
            return goodsDescription.getReturn().getResult().getData().isDeclarable() == true;
        } catch (Exception e) {
            // just continue
        }
        return false;
    }

    public CustomDTO getTariffRateImmediate(String hsCode, Integer countryId) {
        var country = countryRepository.getById(countryId);
        String iso = country.orElseThrow().getIsoCode().name();

        List<CustomMeasureDTO> customMeasures = new ArrayList<>();

        try {
            var measForWsResponse = eUTaxationApiService.getGoodsMeasures(hsCode, iso.toUpperCase(), "I");

            GoodsMeasuresForWsResponse.Measures.Measure selectedMeasure = null;
            Double selectedDuty = null;
            int rank = Integer.MAX_VALUE;

            var measures = filterToNewestMeasuresPerType(measForWsResponse.join().getReturn().getResult().getMeasures().getMeasure());


            for (var measure : measures) {
                var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType());
                boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false);

                if (maybeRelevant) {
                    var duty = extractDuty(measure);

                    if (rank > measureType.get().ordinal() && duty.isPresent()) {
                        rank = measureType.get().ordinal();
                        selectedDuty = duty.get();
                        selectedMeasure = measure;
                    }
                }

                customMeasures.add(new CustomMeasureDTO(
                        measure.getMeasureType().getMeasureType(),
                        measure.getMeasureType().getDescription(),
                        measure.getRegulationId(),
                        measure.getDutyRate()));
            }

            if (selectedDuty != null) {
                return new CustomDTO(false, selectedDuty, customMeasures, countryId);
            }


        } catch (Exception e) {
            // just continue
        }

        return propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.TARIFF_RATE).map(PropertyDTO::getCurrentValue).map(Double::valueOf).map(d -> new CustomDTO(d, customMeasures, countryId)).orElseThrow(() -> new InternalErrorException("Unable to load default custom rate. Please contact support."));

    }

    public CompletableFuture<CustomDTO> getTariffRate(String hsCode, Integer countryId) {
        return CompletableFuture.completedFuture(getTariffRateImmediate(hsCode, countryId));
    }

    public List<TaxationResolverResponse> getTariffRates(List<TaxationResolverRequest> requests) {
        var goodMeasures = doRequests(requests);
        return goodMeasures.keySet().stream().map(r -> mapToResponse(r, goodMeasures.get(r))).toList();
    }

    private TaxationResolverResponse mapToResponse(TaxationResolverRequest request, TaxationResolverApiResponse apiResponse) {

        // source is EU country.
        if(!apiResponse.requestExecuted)
            return new TaxationResolverResponse(0.0, null, request.material().getHsCode(), request.material(), request.countryId());


        List<GoodsMeasForWsResponse> measForWsResponse = apiResponse.apiResponse();

        try {
            String selectedHsCode = null;
            Double selectedDuty = null;
            String selectedMeasure = null;
            double maxDuty = Double.MIN_VALUE;
            double minDuty = Double.MAX_VALUE;
            int rank = Integer.MAX_VALUE;


            var resp = measForWsResponse.stream().collect(
                    Collectors.toMap(
                            r -> r,
                            r -> filterToNewestMeasuresPerType(r.getReturn().getResult().getMeasures().getMeasure())
                    ));

            for (var entry : resp.entrySet()) {
                for (var measure : entry.getValue()) {
                    var measureType = MeasureType.fromMeasureCode(measure.getMeasureType().getMeasureType());
                    boolean maybeRelevant = measureType.map(MeasureType::containsRelevantDuty).orElse(false);

                    if (maybeRelevant) {
                        var duty = extractDuty(measure);

                        if (duty.isPresent()) {

                            maxDuty = Math.max(maxDuty, duty.get());
                            minDuty = Math.min(minDuty, duty.get());

                            if (rank > measureType.get().ordinal()) {
                                rank = measureType.get().ordinal();
                                selectedDuty = duty.get();
                                selectedMeasure = measureType.map(MeasureType::getMeasureCode).orElse(null);
                                selectedHsCode = entry.getKey().getReturn().getResult().getRequest().getGoodsCode();
                            }
                        }
                    }
                }
            }

            if (selectedDuty != null && (maxDuty - minDuty <= 0.02)) {
                return new TaxationResolverResponse(selectedDuty, selectedMeasure, selectedHsCode, request.material(), request.countryId());
            }


        } catch (Exception e) {
            // just continue
        }

        return new TaxationResolverResponse(null, null, request.material().getHsCode(), request.material(), request.countryId());
    }


    public record TaxationResolverSingleRequest(String hsCode, Integer countryId, TaxationResolverRequest origin) {
    }

    public record TaxationResolverRequest(Material material, Integer countryId) {
    }

    public record TaxationResolverApiResponse(Material material, boolean requestExecuted,
                                              List<GoodsMeasForWsResponse> apiResponse) {
    }

    public record TaxationResolverResponse(@Nullable Double tariffRate,
                                           @Nullable String measureCode,
                                           @Nullable String actualHsCode,
                                           Material material,
                                           Integer countryId) {
    }

}
