package ru.yandex.qe.dispenser.ws.quota.request.owning_cost.formula;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableMap;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import ru.yandex.qe.dispenser.api.util.EnumUtils;
import ru.yandex.qe.dispenser.api.v1.DiUnit;
import ru.yandex.qe.dispenser.domain.QuotaChangeRequest;
import ru.yandex.qe.dispenser.ws.bot.Provider;
import ru.yandex.qe.dispenser.ws.quota.request.owning_cost.ChangeOwningCostContext;
import ru.yandex.qe.dispenser.ws.quota.request.owning_cost.pricing.PricingModel;
import ru.yandex.qe.dispenser.ws.quota.request.owning_cost.pricing.QuotaChangeOwningCostTariffManager;

@Component
public class SaasOwningCostFormula implements ProviderOwningCostFormula {

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

    private static final Provider provider = Provider.SAAS;

    private static final String CPU_SKU = "saas.cpu.allocated";
    private static final String RAM_SKU = "saas.memory.allocated";
    private static final String HDD_SKU = "saas.hdd_storage.allocated";
    private static final String SSD_SKU = "saas.ssd_storage.allocated";

    private static final String YP_CPU_SKU = "yp.cpu.quota";
    private static final String YP_RAM_SKU = "yp.memory.quota";
    private static final String YP_HDD_SKU = "yp.hdd_storage.quota";
    private static final String YP_SSD_SKU = "yp.ssd_storage.quota";

    private static final Map<Resource, Map<String, BigDecimal>> SKU_COEFFICIENT_BY_RESOURCE = Map.of(
            Resource.CPU, Map.of(
                    YP_CPU_SKU, BigDecimal.ONE,
                    YP_HDD_SKU, BigDecimal.valueOf(28)
            ),
            Resource.RAM, Map.of(
                    YP_RAM_SKU, BigDecimal.ONE,
                    YP_HDD_SKU, BigDecimal.valueOf(7)
            ),
            Resource.SSD, Map.of(
                    YP_SSD_SKU, BigDecimal.ONE,
                    YP_HDD_SKU, new BigDecimal("0.5", MATH_CONTEXT)
            )
    );

    private final QuotaChangeOwningCostTariffManager quotaChangeOwningCostTariffManager;

    public SaasOwningCostFormula(QuotaChangeOwningCostTariffManager quotaChangeOwningCostTariffManager) {
        this.quotaChangeOwningCostTariffManager = quotaChangeOwningCostTariffManager;
    }

    @Override
    public @NotNull String getProviderKey() {
        return provider.getServiceKey();
    }

    @Override
    public @NotNull Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculateOwningCostFromContext(
            @NotNull Collection<ChangeOwningCostContext> changes) {
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        Map<CampaignKey, List<ChangeOwningCostContext>> changesByCampaign = ProviderOwningCostFormula
                .groupByCampaign(changes, result);
        changesByCampaign.forEach((campaignKey, campaignChanges) -> {
            Map<QuotaChangeRequest.ChangeKey, BigDecimal> campaignCosts = switch (campaignKey) {
                case AUG_2022_DRAFT, AUG_2022_AGGREGATED -> calculateCost(campaignKey, Provider.YP, campaignChanges,
                        this::toOwningCost2022);
                default -> calculateCost(campaignKey, provider, campaignChanges, this::toOwningCost);
            };
            result.putAll(campaignCosts);
        });
        return result;
    }

    @NotNull
    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculateCost(
            CampaignKey campaignKey,
            Provider provider,
            List<ChangeOwningCostContext> campaignChanges,
            BiFunction<QuotaChangeRequest.Change, Map<String, ? extends PricingModel>, BigDecimal> calculator
    ) {
        Map<String, ? extends PricingModel> pricingBySKU =
                quotaChangeOwningCostTariffManager.getByProviderCampaign(provider, campaignKey.getKey()).stream()
                        .collect(Collectors.toMap(PricingModel::getSKU, Function.identity()));
        return campaignChanges.stream()
                .map(ChangeOwningCostContext::getChange)
                .collect(Collectors.toMap(QuotaChangeRequest.ChangeAmount::getKey,
                        change -> calculator.apply(change, pricingBySKU)));
    }

    private BigDecimal toOwningCost(QuotaChangeRequest.Change change,
                                    Map<String, ? extends PricingModel> pricingBySKU) {
        if (pricingBySKU.isEmpty()) {
            return DEFAULT_OWNING_COST;
        }
        BigDecimal result = DEFAULT_OWNING_COST;
        String publicKey = change.getResource().getPublicKey();
        Optional<Resource> resourcesO = Resource.byKey(publicKey);
        if (resourcesO.isPresent()) {
            switch (resourcesO.get()) {
                case SSD:
                    result = calculateForSsd(change, pricingBySKU);
                    break;
                case HDD:
                    result = calculateForHdd(change, pricingBySKU);
                    break;
                case CPU:
                    result = calculateForCpu(change, pricingBySKU);
                    break;
                case RAM:
                    result = calculateForRam(change, pricingBySKU);
                    break;
                case IO_HDD:
                case IO_SSD:
                    break;
            }
        }
        return result;
    }

    private BigDecimal toOwningCost2022(QuotaChangeRequest.Change change,
                                        Map<String, ? extends PricingModel> pricingBySKU) {
        if (pricingBySKU.isEmpty()) {
            return DEFAULT_OWNING_COST;
        }
        BigDecimal result = DEFAULT_OWNING_COST;
        String publicKey = change.getResource().getPublicKey();
        Optional<Resource> resourcesO = Resource.byKey(publicKey);
        if (resourcesO.isPresent()) {
            Resource resource = resourcesO.get();
            DiUnit unit = switch (resource) {
                case CPU -> DiUnit.CORES;
                case RAM, HDD, SSD -> DiUnit.GIBIBYTE;
                case IO_HDD, IO_SSD, IO_NET -> DiUnit.MIBPS;
            };
            result = calculateChange2022(change, resource, unit, pricingBySKU);
        }
        return result;
    }

    private BigDecimal calculateChange2022(QuotaChangeRequest.Change change,
                                           Resource resource,
                                           DiUnit unit,
                                           Map<String, ? extends PricingModel> prisingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        Map<String, BigDecimal> skuCoefficient = SKU_COEFFICIENT_BY_RESOURCE.getOrDefault(resource, Map.of());
        if (prisingModelsBySKU.keySet().containsAll(skuCoefficient.keySet())) {
            BigDecimal price = skuCoefficient.entrySet()
                    .stream()
                    .map(e -> prisingModelsBySKU.get(e.getKey()).getPrice().multiply(e.getValue(), MATH_CONTEXT))
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            result = convert(change, unit, LOG).multiply(price, MATH_CONTEXT);
        }
        return result;
    }

    private BigDecimal calculateForCpu(QuotaChangeRequest.Change change,
                                       Map<String, ? extends PricingModel> prisingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (prisingModelsBySKU.containsKey(CPU_SKU)) {
            PricingModel pricingModel = prisingModelsBySKU.get(CPU_SKU);
            result = convert(change, DiUnit.CORES, LOG)
                    .multiply(pricingModel.getPrice(), MATH_CONTEXT);
        }
        return result;
    }

    private BigDecimal calculateForRam(QuotaChangeRequest.Change change,
                                       Map<String, ? extends PricingModel> prisingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (prisingModelsBySKU.containsKey(RAM_SKU)) {
            PricingModel pricingModel = prisingModelsBySKU.get(RAM_SKU);
            result = convert(change, DiUnit.GIBIBYTE, LOG)
                    .multiply(pricingModel.getPrice(), MATH_CONTEXT);
        }
        return result;
    }

    private BigDecimal calculateForHdd(QuotaChangeRequest.Change change,
                                       Map<String, ? extends PricingModel> prisingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (prisingModelsBySKU.containsKey(HDD_SKU)) {
            PricingModel pricingModel = prisingModelsBySKU.get(HDD_SKU);
            result = convert(change, DiUnit.GIBIBYTE, LOG)
                    .multiply(pricingModel.getPrice(), MATH_CONTEXT);
        }
        return result;
    }

    private BigDecimal calculateForSsd(QuotaChangeRequest.Change change,
                                       Map<String, ? extends PricingModel> prisingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (prisingModelsBySKU.containsKey(SSD_SKU)) {
            PricingModel pricingModel = prisingModelsBySKU.get(SSD_SKU);
            result = convert(change, DiUnit.GIBIBYTE, LOG)
                    .multiply(pricingModel.getPrice(), MATH_CONTEXT);
        }
        return result;
    }

    private enum Resource implements EnumUtils.StringKey {
        SSD("ssd"),
        HDD("hdd"),
        CPU("cpu"),
        RAM("ram"),
        IO_HDD("io_hdd"),
        IO_SSD("io_ssd"),
        IO_NET("io_net"),
        ;

        private static Map<String, Resource> resourceByKey;
        private final String key;

        Resource(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }

        public static Optional<Resource> byKey(String key) {
            if (Resource.resourceByKey == null) {
                Resource.resourceByKey = ImmutableMap.copyOf(EnumUtils.prepareKeysMap(Resource.values()));
            }
            return Optional.ofNullable(Resource.resourceByKey.get(key));
        }

    }

}
