package ru.yandex.qe.dispenser.ws.base_resources.impl;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;

import ru.yandex.qe.dispenser.api.v1.DiUnit;
import ru.yandex.qe.dispenser.domain.QuotaChangeRequest;
import ru.yandex.qe.dispenser.domain.base_resources.BaseResourceMapping;
import ru.yandex.qe.dispenser.domain.base_resources.BigOrderMappings;
import ru.yandex.qe.dispenser.domain.base_resources.CampaignMappings;
import ru.yandex.qe.dispenser.domain.base_resources.Fraction;
import ru.yandex.qe.dispenser.domain.base_resources.MdsLocation;
import ru.yandex.qe.dispenser.domain.base_resources.MdsStorageType;
import ru.yandex.qe.dispenser.domain.base_resources.ResourceAndSegmentsId;
import ru.yandex.qe.dispenser.domain.mds.MdsConfigApi;
import ru.yandex.qe.dispenser.domain.mds.MdsMarketingStorage;
import ru.yandex.qe.dispenser.domain.mds.MdsRawStorageByDc;
import ru.yandex.qe.dispenser.domain.mds.StorageType;
import ru.yandex.qe.dispenser.ws.bot.mapping.Location;

@Component
public class BaseResourceRelationRemoteEvaluator {

    private static final Logger LOG = LoggerFactory.getLogger(BaseResourceRelationRemoteEvaluator.class);

    private final static long BACK_OFF_PERIOD = 100L;
    private final static int MAX_ATTEMPTS = 3;

    private final MdsConfigApi mdsConfigApi;
    private final RetryTemplate retryTemplate;

    public BaseResourceRelationRemoteEvaluator(MdsConfigApi mdsConfigApi) {
        this.mdsConfigApi = mdsConfigApi;
        this.retryTemplate = prepareRetryTemplate();
    }

    public Map<Long, Map<Long, Map<Long, BigDecimal>>> evaluateRemote(
            Map<Long, QuotaChangeRequest> requestsById,
            Map<Long, Map<Long, Map<Long, Map<ResourceAndSegmentsId, QuotaChangeRequest.Change>>>> perRequestPerBigOrderMappingChanges,
            Function<Long, CampaignMappings> campaignMappingsSupplier,
            boolean skipRemoteErrors) {
        if (perRequestPerBigOrderMappingChanges.isEmpty()) {
            return Map.of();
        }
        Map<Long, Map<Long, Map<Long, BigDecimal>>> perRequestPerBigOrderEvaluatedMappings = new HashMap<>();
        Set<MdsMarketingStorage> marketingStorage = prepareMdsMarketingStorages(requestsById,
                perRequestPerBigOrderMappingChanges, campaignMappingsSupplier);
        List<MdsRawStorageByDc> rawStorage = getMdsRawStorageByDc(marketingStorage, skipRemoteErrors);
        evaluateMdsMappings(requestsById, perRequestPerBigOrderMappingChanges, campaignMappingsSupplier,
                perRequestPerBigOrderEvaluatedMappings, rawStorage);
        return perRequestPerBigOrderEvaluatedMappings;
    }

    public Map<Long, Map<Long, BigDecimal>> evaluateRemote(
            QuotaChangeRequest request,
            Map<Long, Map<Long, Map<ResourceAndSegmentsId, QuotaChangeRequest.Change>>> perBigOrderMappingChanges,
            CampaignMappings campaignMappings,
            boolean skipRemoteErrors) {
        if (perBigOrderMappingChanges.isEmpty()) {
            return Map.of();
        }
        Map<Long, Map<Long, BigDecimal>> perBigOrderEvaluatedMappings = new HashMap<>();
        Set<MdsMarketingStorage> marketingStorage = prepareMdsMarketingStorages(request, perBigOrderMappingChanges,
                campaignMappings);
        List<MdsRawStorageByDc> rawStorage = getMdsRawStorageByDc(marketingStorage, skipRemoteErrors);
        evaluateMdsMappings(perBigOrderMappingChanges, campaignMappings, perBigOrderEvaluatedMappings, rawStorage);
        return perBigOrderEvaluatedMappings;
    }

    private Set<MdsMarketingStorage> prepareMdsMarketingStorages(
        Map<Long, QuotaChangeRequest> requestsById,
        Map<Long, Map<Long, Map<Long, Map<ResourceAndSegmentsId, QuotaChangeRequest.Change>>>> perRequestPerBigOrderMappingChanges,
        Function<Long, CampaignMappings> campaignMappingsSupplier) {
        Set<MdsMarketingStorage> marketingStorage = new HashSet<>();
        perRequestPerBigOrderMappingChanges.forEach((quotaRequestId, perBigOrderMappingChanges) -> {
            QuotaChangeRequest request = requestsById.get(quotaRequestId);
            CampaignMappings campaignMappings = campaignMappingsSupplier.apply(request.getCampaignId());
            Integer abcServiceId = Objects.requireNonNull(request.getProject().getAbcServiceId(),
                    () -> String.format("Project %s has no abc service id",
                            request.getProject().getPublicKey()));
            perBigOrderMappingChanges.forEach((bigOrderId, mappings) -> {
                BigOrderMappings bigOrderMappings = campaignMappings.getPerBigOrderMappings().get(bigOrderId);
                mappings.forEach((mappingId, changes) -> {
                    BaseResourceMapping mapping = bigOrderMappings.getMappingsById().get(mappingId);
                    if (mapping == null) {
                        return;
                    }
                    if (mapping.getRelation().getMds().isPresent()) {
                        mapping.getRelation().getMds().get().getTerms().forEach(term -> {
                            ResourceAndSegmentsId key = new ResourceAndSegmentsId(term.getResourceId(),
                                    term.getSegmentIds());
                            QuotaChangeRequest.Change change = changes.get(key);
                            if (change == null || change.getAmount() == 0L) {
                                return;
                            }
                            long amountBytes = DiUnit.BYTE.convert(change.getAmount(),
                                    change.getResource().getType().getBaseUnit());
                            StorageType storageType = toStorageType(term.getStorageType());
                            long actualAbcServiceId = term.getAbcServiceId().orElse((long) abcServiceId);
                            MdsMarketingStorage storage = new MdsMarketingStorage(change.getId(), actualAbcServiceId,
                                    storageType, amountBytes);
                            marketingStorage.add(storage);
                        });
                    }
                });
            });
        });
        return marketingStorage;
    }

    private Set<MdsMarketingStorage> prepareMdsMarketingStorages(
            QuotaChangeRequest request,
            Map<Long, Map<Long, Map<ResourceAndSegmentsId, QuotaChangeRequest.Change>>> perBigOrderMappingChanges,
            CampaignMappings campaignMappings) {
        Set<MdsMarketingStorage> marketingStorage = new HashSet<>();
        Integer abcServiceId = Objects.requireNonNull(request.getProject().getAbcServiceId(),
                () -> String.format("Project %s has no abc service id",
                        request.getProject().getPublicKey()));
        perBigOrderMappingChanges.forEach((bigOrderId, mappings) -> {
            BigOrderMappings bigOrderMappings = campaignMappings.getPerBigOrderMappings().get(bigOrderId);
            mappings.forEach((mappingId, changes) -> {
                BaseResourceMapping mapping = bigOrderMappings.getMappingsById().get(mappingId);
                if (mapping == null) {
                    return;
                }
                if (mapping.getRelation().getMds().isPresent()) {
                    mapping.getRelation().getMds().get().getTerms().forEach(term -> {
                        ResourceAndSegmentsId key = new ResourceAndSegmentsId(term.getResourceId(),
                                term.getSegmentIds());
                        QuotaChangeRequest.Change change = changes.get(key);
                        if (change == null || change.getAmount() == 0L) {
                            return;
                        }
                        long amountBytes = DiUnit.BYTE.convert(change.getAmount(),
                                change.getResource().getType().getBaseUnit());
                        StorageType storageType = toStorageType(term.getStorageType());
                        long actualAbcServiceId = term.getAbcServiceId().orElse((long) abcServiceId);
                        MdsMarketingStorage storage = new MdsMarketingStorage(change.getId(), actualAbcServiceId,
                                storageType, amountBytes);
                        marketingStorage.add(storage);
                    });
                }
            });
        });
        return marketingStorage;
    }

    private List<MdsRawStorageByDc> getMdsRawStorageByDc(Set<MdsMarketingStorage> marketingStorage,
                                                         boolean skipRemoteErrors) {
        if (marketingStorage.isEmpty()) {
            return List.of();
        }
        List<MdsRawStorageByDc> rawStorage;
        try {
            rawStorage = retryTemplate.execute(ctx -> mdsConfigApi.calculateRawStorageByDc(marketingStorage));
        } catch (Exception e) {
            LOG.error("MDS storage convert error", e);
            if (skipRemoteErrors) {
                rawStorage = List.of();
            } else {
                throw e;
            }
        }
        return rawStorage;
    }

    private void evaluateMdsMappings(
            Map<Long, QuotaChangeRequest> requestsById,
            Map<Long, Map<Long, Map<Long, Map<ResourceAndSegmentsId, QuotaChangeRequest.Change>>>> perRequestPerBigOrderMappingChanges,
            Function<Long, CampaignMappings> campaignMappingsSupplier,
            Map<Long, Map<Long, Map<Long, BigDecimal>>> perRequestPerBigOrderEvaluatedMappings,
            List<MdsRawStorageByDc> rawStorage) {
        if (rawStorage.isEmpty()) {
            return;
        }
        Map<Long, Map<MdsLocation, Long>> rawStorageByChangeIdLocation = new HashMap<>();
        rawStorage.forEach(storage -> {
            if (storage == null) {
                return;
            }
            storage.getStorage().forEach(dcStorage -> {
                if (dcStorage == null) {
                    return;
                }
                Optional<MdsLocation> location = fromLocation(dcStorage.getLocation());
                if (location.isEmpty()) {
                    return;
                }
                rawStorageByChangeIdLocation.computeIfAbsent(storage.getId(), k -> new HashMap<>())
                        .put(location.get(), dcStorage.getAmount());
            });
        });
        perRequestPerBigOrderMappingChanges.forEach((quotaRequestId, perBigOrderMappingChanges) -> {
            QuotaChangeRequest request = requestsById.get(quotaRequestId);
            CampaignMappings campaignMappings = campaignMappingsSupplier.apply(request.getCampaignId());
            perBigOrderMappingChanges.forEach((bigOrderId, mappings) -> {
                BigOrderMappings bigOrderMappings = campaignMappings.getPerBigOrderMappings().get(bigOrderId);
                mappings.forEach((mappingId, changes) -> {
                    BaseResourceMapping mapping = bigOrderMappings.getMappingsById().get(mappingId);
                    if (mapping == null) {
                        return;
                    }
                    List<Fraction> termFractions = new ArrayList<>();
                    if (mapping.getRelation().getMds().isPresent()) {
                        mapping.getRelation().getMds().get().getTerms().forEach(term -> {
                            ResourceAndSegmentsId key = new ResourceAndSegmentsId(term.getResourceId(),
                                    term.getSegmentIds());
                            QuotaChangeRequest.Change change = changes.get(key);
                            if (change == null) {
                                return;
                            }
                            long amountBytes = rawStorageByChangeIdLocation.getOrDefault(change.getId(), Map.of())
                                    .getOrDefault(term.getLocation(), 0L);
                            if (amountBytes == 0L) {
                                return;
                            }
                            termFractions.add(new Fraction(BigInteger.valueOf(amountBytes).multiply(BigInteger
                                    .valueOf(term.getNumerator())), BigInteger.valueOf(term.getDenominator())));
                        });
                    }
                    List<Fraction> nonZeroTerms = termFractions.stream()
                            .filter(f -> f.getNumerator().compareTo(BigInteger.ZERO) != 0)
                            .collect(Collectors.toList());
                    if (!nonZeroTerms.isEmpty()) {
                        Fraction sum = nonZeroTerms.get(0);
                        if (nonZeroTerms.size() > 1) {
                            for (int i = 1; i < nonZeroTerms.size(); i++) {
                                Fraction term = nonZeroTerms.get(i);
                                if (sum.getDenominator().compareTo(term.getDenominator()) == 0) {
                                    sum = new Fraction(sum.getNumerator().add(term.getNumerator()), sum.getDenominator());
                                } else {
                                    BigInteger commonDenominator = sum.getDenominator().multiply(term.getDenominator());
                                    BigInteger sumNumerator = sum.getNumerator().multiply(term.getDenominator());
                                    BigInteger termNumerator = term.getNumerator().multiply(sum.getDenominator());
                                    sum = new Fraction(sumNumerator.add(termNumerator), commonDenominator);
                                }
                            }
                        }
                        BigDecimal result = new BigDecimal(sum.getNumerator()).divide(new BigDecimal(sum.getDenominator()),
                                0, RoundingMode.UP);
                        perRequestPerBigOrderEvaluatedMappings
                                .computeIfAbsent(quotaRequestId, k -> new HashMap<>())
                                .computeIfAbsent(bigOrderId, k -> new HashMap<>())
                                .put(mappingId, result);
                    }
                });
            });
        });
    }

    private void evaluateMdsMappings(
            Map<Long, Map<Long, Map<ResourceAndSegmentsId, QuotaChangeRequest.Change>>> perBigOrderMappingChanges,
            CampaignMappings campaignMappings,
            Map<Long, Map<Long, BigDecimal>> perBigOrderEvaluatedMappings,
            List<MdsRawStorageByDc> rawStorage) {
        if (rawStorage.isEmpty()) {
            return;
        }
        Map<Long, Map<MdsLocation, Long>> rawStorageByChangeIdLocation = new HashMap<>();
        rawStorage.forEach(storage -> {
            if (storage == null) {
                return;
            }
            storage.getStorage().forEach(dcStorage -> {
                if (dcStorage == null) {
                    return;
                }
                Optional<MdsLocation> location = fromLocation(dcStorage.getLocation());
                if (location.isEmpty()) {
                    return;
                }
                rawStorageByChangeIdLocation.computeIfAbsent(storage.getId(), k -> new HashMap<>())
                        .put(location.get(), dcStorage.getAmount());
            });
        });
        perBigOrderMappingChanges.forEach((bigOrderId, mappings) -> {
            BigOrderMappings bigOrderMappings = campaignMappings.getPerBigOrderMappings().get(bigOrderId);
            mappings.forEach((mappingId, changes) -> {
                BaseResourceMapping mapping = bigOrderMappings.getMappingsById().get(mappingId);
                if (mapping == null) {
                    return;
                }
                List<Fraction> termFractions = new ArrayList<>();
                if (mapping.getRelation().getMds().isPresent()) {
                    mapping.getRelation().getMds().get().getTerms().forEach(term -> {
                        ResourceAndSegmentsId key = new ResourceAndSegmentsId(term.getResourceId(),
                                term.getSegmentIds());
                        QuotaChangeRequest.Change change = changes.get(key);
                        if (change == null) {
                            return;
                        }
                        long amountBytes = rawStorageByChangeIdLocation.getOrDefault(change.getId(), Map.of())
                                .getOrDefault(term.getLocation(), 0L);
                        if (amountBytes == 0L) {
                            return;
                        }
                        termFractions.add(new Fraction(BigInteger.valueOf(amountBytes).multiply(BigInteger
                                .valueOf(term.getNumerator())), BigInteger.valueOf(term.getDenominator())));
                    });
                }
                List<Fraction> nonZeroTerms = termFractions.stream()
                        .filter(f -> f.getNumerator().compareTo(BigInteger.ZERO) != 0)
                        .collect(Collectors.toList());
                if (!nonZeroTerms.isEmpty()) {
                    Fraction sum = nonZeroTerms.get(0);
                    if (nonZeroTerms.size() > 1) {
                        for (int i = 1; i < nonZeroTerms.size(); i++) {
                            Fraction term = nonZeroTerms.get(i);
                            if (sum.getDenominator().compareTo(term.getDenominator()) == 0) {
                                sum = new Fraction(sum.getNumerator().add(term.getNumerator()), sum.getDenominator());
                            } else {
                                BigInteger commonDenominator = sum.getDenominator().multiply(term.getDenominator());
                                BigInteger sumNumerator = sum.getNumerator().multiply(term.getDenominator());
                                BigInteger termNumerator = term.getNumerator().multiply(sum.getDenominator());
                                sum = new Fraction(sumNumerator.add(termNumerator), commonDenominator);
                            }
                        }
                    }
                    BigDecimal result = new BigDecimal(sum.getNumerator()).divide(new BigDecimal(sum.getDenominator()),
                            0, RoundingMode.UP);
                    perBigOrderEvaluatedMappings.computeIfAbsent(bigOrderId, k -> new HashMap<>())
                            .put(mappingId, result);
                }
            });
        });
    }

    private StorageType toStorageType(MdsStorageType type) {
        switch (type) {
            case S3:
                return StorageType.S3;
            case MDS:
                return StorageType.MDS;
            case AVATARS:
                return StorageType.AVATARS;
            default:
                throw new IllegalArgumentException("Unexpected storage type: " + type);
        }
    }

    private Optional<MdsLocation> fromLocation(String location) {
        if (location == null) {
            return Optional.empty();
        }
        Location locationEnum = Location.fromKey(location);
        if (locationEnum == null) {
            return Optional.empty();
        }
        switch (locationEnum) {
            case MAN:
                return Optional.of(MdsLocation.MAN);
            case SAS:
                return Optional.of(MdsLocation.SAS);
            case VLA:
                return Optional.of(MdsLocation.VLA);
            case MYT:
                return Optional.of(MdsLocation.MYT);
            case IVA:
                return Optional.of(MdsLocation.IVA);
            default:
                return Optional.empty();
        }
    }

    private static RetryTemplate prepareRetryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(BACK_OFF_PERIOD);
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(MAX_ATTEMPTS);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
        retryTemplate.setRetryPolicy(retryPolicy);
        return retryTemplate;
    }

}
