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

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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.beans.factory.annotation.Value;
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.domain.Segment;
import ru.yandex.qe.dispenser.domain.dao.segment.SegmentUtils;
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 SandboxOwningCostFormula implements ProviderOwningCostFormula {

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

    private static final Provider PROVIDER = Provider.SANDBOX;
    private static final BigDecimal S3_STORAGE_TARIFF_BY_GIB = new BigDecimal("0.1047", MATH_CONTEXT);
    private static final String SLOT_GUARANTEE_HDD_SKU = "sandbox.compute.guarantee.slots_hdd_segment";
    private static final String SLOT_GUARANTEE_MACOS_SKU = "sandbox.compute.guarantee.slots_macos_segment";
    private static final String SLOT_GUARANTEE_SSD_SKU = "sandbox.compute.guarantee.slots_ssd_segment";
    private static final String SLOT_GUARANTEE_WINDOWS_SKU = "sandbox.compute.guarantee.slots_windows_segment";
    private static final String SLOT_USAGE_HDD_SKU = "sandbox.compute.usage.slots_hdd_segment";
    private static final String SLOT_USAGE_MACOS_SKU = "sandbox.compute.usage.slots_macos_segment";
    private static final String SLOT_USAGE_SSD_SKU = "sandbox.compute.usage.slots_ssd_segment";
    private static final String SLOT_USAGE_WINDOWS_SKU = "sandbox.compute.usage.slots_windows_segment";
    private static final BigDecimal CORES_PER_MAC_MINI = BigDecimal.valueOf(8L);
    private static final BigDecimal CORES_PER_WINDOWS = BigDecimal.valueOf(16L);
    private static final BigDecimal RAM_BYTES_PER_LINUX_CORE = BigDecimal.valueOf(4L)
            .multiply(BigDecimal.valueOf(1024L).pow(3, MATH_CONTEXT), MATH_CONTEXT);
    private static final BigDecimal SSD_BYTES_PER_LINUX_CORE = new BigDecimal("11.7", MATH_CONTEXT)
            .multiply(BigDecimal.valueOf(10L).pow(9, MATH_CONTEXT), MATH_CONTEXT);
    private static final BigDecimal HDD_BYTES_PER_LINUX_CORE = new BigDecimal("250", MATH_CONTEXT)
            .multiply(BigDecimal.valueOf(10L).pow(9, MATH_CONTEXT), MATH_CONTEXT);
    private static final BigDecimal HOURS_PER_MONTH = BigDecimal.valueOf(720L);
    private static final String HDD_X2_SKU = "s3.quota_space.hdd.x2";

    private final QuotaChangeOwningCostTariffManager quotaChangeOwningCostTariffManager;
    private final String sandboxOldSegmentationKey;

    public SandboxOwningCostFormula(
            QuotaChangeOwningCostTariffManager quotaChangeOwningCostTariffManager,
            @Value("${dispenser.sandbox.old.segmentation.key}") String sandboxOldSegmentationKey) {
        this.quotaChangeOwningCostTariffManager = quotaChangeOwningCostTariffManager;
        this.sandboxOldSegmentationKey = sandboxOldSegmentationKey;
    }

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

    @Override
    public @NotNull Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculateOwningCostFromContext(
            @NotNull Collection<ChangeOwningCostContext> changes) {
        LOG.info("Calculating sandbox owning cost for {}", changes);
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        Map<CampaignKey, List<ChangeOwningCostContext>> changesByCampaign = ProviderOwningCostFormula
                .groupByCampaign(changes, result);
        changesByCampaign.forEach((campaignKey, campaignChanges) -> {
            Map<String, ? extends PricingModel> pricingBySKU = quotaChangeOwningCostTariffManager
                    .getByProvidersCampaign(List.of(Provider.SANDBOX, Provider.S3), campaignKey.getKey()).stream()
                    .collect(Collectors.toMap(PricingModel::getSKU, Function.identity()));
            Map<QuotaChangeRequest.ChangeKey, BigDecimal> campaignCosts = calculateCampaignOwningCost(
                    campaignKey, campaignChanges, pricingBySKU);
            LOG.info("Calculated sandbox owning cost for campaign {}: {}", campaignKey, campaignCosts);
            result.putAll(campaignCosts);
        });
        LOG.info("Calculated sandbox owning cost for quota request: {}", result);
        return result;
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculateCampaignOwningCost(
            CampaignKey campaignKey, List<ChangeOwningCostContext> changes,
            Map<String, ? extends PricingModel> pricingBySKU) {
        LOG.info("Calculating sandbox owning cost in campaign {} for {}", campaignKey, changes);
        switch (campaignKey) {
            case AUG2020:
            case FEB2021:
                return calculate2020CampaignOwningCost(changes, pricingBySKU);
            case AUG2021:
                return calculate2021CampaignOwningCost(changes, pricingBySKU);
            case AUG_2022_DRAFT:
            case AUG_2022_AGGREGATED:
                return calculate2022CampaignOwningCost(changes, pricingBySKU);
            default:
                return changes.stream().collect(Collectors.toMap(change -> change.getChange().getKey(),
                        change -> DEFAULT_OWNING_COST));
        }
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculate2022CampaignOwningCost(
            List<ChangeOwningCostContext> changes, Map<String, ? extends PricingModel> pricingBySKU) {
        return changes.stream()
                .map(ChangeOwningCostContext::getChange)
                .collect(Collectors.toMap(QuotaChangeRequest.ChangeAmount::getKey,
                        change -> toOwningCost2022(change, pricingBySKU)));
    }

    private BigDecimal toOwningCost2022(QuotaChangeRequest.Change change,
                                        Map<String, ? extends PricingModel> pricingBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (pricingBySKU.isEmpty()) {
            return result;
        }
        String publicKey = change.getResource().getPublicKey();
        Optional<Resource2022> resourcesO = Resource2022.byKey(publicKey);
        if (resourcesO.isPresent()) {
            switch (resourcesO.get()) {
                case CPU_LINUX_HDD -> result = calculateForLinuxHddCpu2022(change, pricingBySKU);
                case CPU_LINUX_SSD -> result = calculateForLinuxSsdCpu2022(change, pricingBySKU);
                case CPU_MACOS -> result = calculateForMacosCpu2022(change, pricingBySKU);
                case CPU_WINDOWS -> result = calculateForWindowsCpu2022(change, pricingBySKU);
                case S3_HDD_X2 -> result = calculate2022ForS3HddX2(change, pricingBySKU);
            }
        }
        return result;
    }

    private BigDecimal calculateForLinuxHddCpu2022(QuotaChangeRequest.Change change,
                                           Map<String, ? extends PricingModel> pricingBySKU) {
        PricingModel guaranteePricing = pricingBySKU.get(SLOT_GUARANTEE_HDD_SKU);
        PricingModel usagePricing = pricingBySKU.get(SLOT_USAGE_HDD_SKU);
        if (guaranteePricing == null || usagePricing == null) {
            return ProviderOwningCostFormula.DEFAULT_OWNING_COST;
        }
        return convert(change, DiUnit.CORES, LOG)
                .multiply(guaranteePricing.getPrice().add(usagePricing.getPrice(), MATH_CONTEXT), MATH_CONTEXT);
    }

    private BigDecimal calculateForLinuxSsdCpu2022(QuotaChangeRequest.Change change,
                                                   Map<String, ? extends PricingModel> pricingBySKU) {
        PricingModel guaranteePricing = pricingBySKU.get(SLOT_GUARANTEE_SSD_SKU);
        PricingModel usagePricing = pricingBySKU.get(SLOT_USAGE_SSD_SKU);
        if (guaranteePricing == null || usagePricing == null) {
            return ProviderOwningCostFormula.DEFAULT_OWNING_COST;
        }
        return convert(change, DiUnit.CORES, LOG)
                .multiply(guaranteePricing.getPrice().add(usagePricing.getPrice(), MATH_CONTEXT), MATH_CONTEXT);
    }

    private BigDecimal calculateForMacosCpu2022(QuotaChangeRequest.Change change,
                                                Map<String, ? extends PricingModel> pricingBySKU) {
        PricingModel guaranteePricing = pricingBySKU.get(SLOT_GUARANTEE_MACOS_SKU);
        PricingModel usagePricing = pricingBySKU.get(SLOT_USAGE_MACOS_SKU);
        if (guaranteePricing == null || usagePricing == null) {
            return ProviderOwningCostFormula.DEFAULT_OWNING_COST;
        }
        return convert(change, DiUnit.CORES, LOG)
                .multiply(guaranteePricing.getPrice().add(usagePricing.getPrice(), MATH_CONTEXT), MATH_CONTEXT);
    }

    private BigDecimal calculateForWindowsCpu2022(QuotaChangeRequest.Change change,
                                                  Map<String, ? extends PricingModel> pricingBySKU) {
        PricingModel guaranteePricing = pricingBySKU.get(SLOT_GUARANTEE_WINDOWS_SKU);
        PricingModel usagePricing = pricingBySKU.get(SLOT_USAGE_WINDOWS_SKU);
        if (guaranteePricing == null || usagePricing == null) {
            return ProviderOwningCostFormula.DEFAULT_OWNING_COST;
        }
        return convert(change, DiUnit.CORES, LOG)
                .multiply(guaranteePricing.getPrice().add(usagePricing.getPrice(), MATH_CONTEXT), MATH_CONTEXT);
    }

    private BigDecimal calculate2022ForS3HddX2(QuotaChangeRequest.Change change,
                                               Map<String, ? extends PricingModel> pricingBySKU) {
        PricingModel pricing = pricingBySKU.get(HDD_X2_SKU);
        if (pricing == null) {
            return ProviderOwningCostFormula.DEFAULT_OWNING_COST;
        }
        return convert(change, DiUnit.GIBIBYTE, LOG)
                .multiply(HOURS_PER_MONTH, ProviderOwningCostFormula.MATH_CONTEXT)
                .multiply(pricing.getPrice(), ProviderOwningCostFormula.MATH_CONTEXT);
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculate2020CampaignOwningCost(
            List<ChangeOwningCostContext> changes, Map<String, ? extends PricingModel> pricingBySKU) {
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        Map<Long, List<ChangeOwningCostContext>> changesByBigOrder = new HashMap<>();
        changes.forEach(change -> {
            if (change.getChange().getBigOrder() != null) {
                changesByBigOrder.computeIfAbsent(change.getChange().getBigOrder().getId(), k -> new ArrayList<>())
                        .add(change);
            } else {
                result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
            }
        });
        changesByBigOrder.forEach((bigOrderId, bigOrderChanges) -> {
            Map<QuotaChangeRequest.ChangeKey, BigDecimal> bigOrderCosts = calculate2020CampaignOrderOwningCost(
                    bigOrderChanges, pricingBySKU);
            LOG.info("Calculated sandbox owning cost for 2020 big order {}: {}", bigOrderId, bigOrderCosts);
            result.putAll(bigOrderCosts);
        });
        return result;
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculate2020CampaignOrderOwningCost(
            List<ChangeOwningCostContext> changes, Map<String, ? extends PricingModel> pricingBySKU) {
        LOG.info("Calculating sandbox owning cost in 2020 big order for {}", changes);
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        Map<Resource2020, ChangeOwningCostContext> linuxBareMetalChanges = new HashMap<>();
        Map<Resource2020, ChangeOwningCostContext> linuxYpChanges = new HashMap<>();
        changes.forEach(change -> {
            Optional<SandboxSegment2020> segmentO = getSegmentFromChange(change.getChange());
            if (segmentO.isEmpty()) {
                result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                return;
            }
            String resourceKey = change.getChange().getResource().getPublicKey();
            Optional<Resource2020> resourceO = Resource2020.byKey(resourceKey);
            if (resourceO.isEmpty()) {
                result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                return;
            }
            switch (segmentO.get()) {
                case SANDBOX_LINUX_BARE_METAL:
                    if (resourceO.get() == Resource2020.CPU_SEGMENTED ||
                            resourceO.get() == Resource2020.RAM_SEGMENTED ||
                            resourceO.get() == Resource2020.HDD_SEGMENTED ||
                            resourceO.get() == Resource2020.SSD_SEGMENTED) {
                        linuxBareMetalChanges.put(resourceO.get(), change);
                    } else {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                    break;
                case SANDBOX_LINUX_YP:
                    if (resourceO.get() == Resource2020.CPU_SEGMENTED ||
                            resourceO.get() == Resource2020.RAM_SEGMENTED ||
                            resourceO.get() == Resource2020.HDD_SEGMENTED ||
                            resourceO.get() == Resource2020.SSD_SEGMENTED) {
                        linuxYpChanges.put(resourceO.get(), change);
                    } else {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                    break;
                case SANDBOX_MAC_MINI:
                    if (resourceO.get() == Resource2020.CPU_SEGMENTED) {
                        result.put(change.getChange().getKey(),
                                calculateForMacMini2020(change.getChange(), pricingBySKU));
                    } else {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                    break;
                case SANDBOX_WINDOWS:
                    if (resourceO.get() == Resource2020.CPU_SEGMENTED) {
                        result.put(change.getChange().getKey(),
                                calculateForWindows2020(change.getChange(), pricingBySKU));
                    } else {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                    break;
                default:
                    result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
            }
        });
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> linuxBareMetalCosts
                = calculate2020CampaignOrderLinuxSegmentOwningCost(linuxBareMetalChanges, pricingBySKU);
        LOG.info("Calculated sandbox owning cost for linux bare metal 2020 big order: {}", linuxBareMetalCosts);
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> linuxYpCosts
                = calculate2020CampaignOrderLinuxSegmentOwningCost(linuxYpChanges, pricingBySKU);
        LOG.info("Calculated sandbox owning cost for linux YP 2020 big order: {}", linuxYpCosts);
        result.putAll(linuxBareMetalCosts);
        result.putAll(linuxYpCosts);
        return result;
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculate2020CampaignOrderLinuxSegmentOwningCost(
            Map<Resource2020, ChangeOwningCostContext> changes, Map<String, ? extends PricingModel> pricingBySKU) {
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        if (!pricingBySKU.containsKey(SLOT_GUARANTEE_SSD_SKU) ||
                !pricingBySKU.containsKey(SLOT_USAGE_SSD_SKU) ||
                !pricingBySKU.containsKey(SLOT_GUARANTEE_HDD_SKU) ||
                !pricingBySKU.containsKey(SLOT_USAGE_HDD_SKU)) {
            changes.forEach((resource, change) -> result.put(change.getChange().getKey(), DEFAULT_OWNING_COST));
            LOG.info("Calculating sandbox 2020 owning cost, no prices, result = {}", result);
            return result;
        }
        BigDecimal ssdSegmentPrice = pricingBySKU.get(SLOT_GUARANTEE_SSD_SKU).getPrice()
                .add(pricingBySKU.get(SLOT_USAGE_SSD_SKU).getPrice(), MATH_CONTEXT);
        BigDecimal hddSegmentPrice = pricingBySKU.get(SLOT_GUARANTEE_HDD_SKU).getPrice()
                .add(pricingBySKU.get(SLOT_USAGE_HDD_SKU).getPrice(), MATH_CONTEXT);
        boolean hasCpu = changes.containsKey(Resource2020.CPU_SEGMENTED);
        boolean hasRam = changes.containsKey(Resource2020.RAM_SEGMENTED);
        boolean hasSsd = changes.containsKey(Resource2020.SSD_SEGMENTED);
        boolean hasHdd = changes.containsKey(Resource2020.HDD_SEGMENTED);
        BigDecimal cpuCores = Optional.ofNullable(changes.get(Resource2020.CPU_SEGMENTED))
                .map(v -> convert(v.getChange(), DiUnit.CORES, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal ramBytes = Optional.ofNullable(changes.get(Resource2020.RAM_SEGMENTED))
                .map(v -> convert(v.getChange(), DiUnit.BYTE, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal hddBytes = Optional.ofNullable(changes.get(Resource2020.HDD_SEGMENTED))
                .map(v -> convert(v.getChange(), DiUnit.BYTE, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal ssdBytes = Optional.ofNullable(changes.get(Resource2020.SSD_SEGMENTED))
                .map(v -> convert(v.getChange(), DiUnit.BYTE, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal coresForRamBytes = ramBytes.divide(RAM_BYTES_PER_LINUX_CORE, MATH_CONTEXT)
                .setScale(0, RoundingMode.UP);
        BigDecimal coresForHddBytes = hddBytes.divide(HDD_BYTES_PER_LINUX_CORE, MATH_CONTEXT)
                .setScale(0, RoundingMode.UP);
        BigDecimal coresForSsdBytes = ssdBytes.divide(SSD_BYTES_PER_LINUX_CORE, MATH_CONTEXT)
                .setScale(0, RoundingMode.UP);
        BigDecimal storageCores = coresForHddBytes.add(coresForSsdBytes, MATH_CONTEXT);
        BigDecimal computeCores = cpuCores.max(coresForRamBytes);
        LOG.info("Calculating sandbox 2020 owning cost, cpu cores = {}, cores for ram bytes = {}, cores for hdd bytes = {}, " +
                "cores for ssd bytes = {}, storage cores = {}, compute cores = {}", cpuCores, coresForRamBytes, coresForHddBytes,
                coresForSsdBytes, storageCores, computeCores);
        if (storageCores.compareTo(computeCores) >= 0) {
            BigDecimal hddSegmentCost = coresForHddBytes.multiply(hddSegmentPrice, MATH_CONTEXT);
            BigDecimal ssdSegmentCost = coresForSsdBytes.multiply(ssdSegmentPrice, MATH_CONTEXT);
            LOG.info("Calculating sandbox 2020 owning cost, storage dominates, hdd segment cost = {}, " +
                    "ssd segment cost = {}", hddSegmentCost, ssdSegmentCost);
            if (hasCpu) {
                result.put(changes.get(Resource2020.CPU_SEGMENTED).getChange().getKey(),
                        hddSegmentCost.add(ssdSegmentCost, MATH_CONTEXT));
                changes.forEach((resource, change) -> {
                    if (resource != Resource2020.CPU_SEGMENTED) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            } else {
                if (hasHdd) {
                    result.put(changes.get(Resource2020.HDD_SEGMENTED).getChange().getKey(), hddSegmentCost);
                }
                if (hasSsd) {
                    result.put(changes.get(Resource2020.SSD_SEGMENTED).getChange().getKey(), ssdSegmentCost);
                }
                changes.forEach((resource, change) -> {
                    if (resource != Resource2020.HDD_SEGMENTED && resource != Resource2020.SSD_SEGMENTED) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            }
            LOG.info("Calculating sandbox 2020 owning cost, storage dominates, result = {}", result);
        } else {
            BigDecimal hddShare;
            BigDecimal ssdShare;
            if (coresForHddBytes.compareTo(BigDecimal.ZERO) == 0 && coresForSsdBytes.compareTo(BigDecimal.ZERO) == 0) {
                hddShare = new BigDecimal("0.0", MATH_CONTEXT);
                ssdShare = new BigDecimal("1.0", MATH_CONTEXT);
            } else {
                hddShare = coresForHddBytes.divide(coresForHddBytes.add(coresForSsdBytes, MATH_CONTEXT), MATH_CONTEXT);
                ssdShare = coresForSsdBytes.divide(coresForHddBytes.add(coresForSsdBytes, MATH_CONTEXT), MATH_CONTEXT);
            }
            BigDecimal hddSegmentCost = computeCores.multiply(hddShare, MATH_CONTEXT)
                    .multiply(hddSegmentPrice, MATH_CONTEXT);
            BigDecimal ssdSegmentCost = computeCores.multiply(ssdShare, MATH_CONTEXT)
                    .multiply(ssdSegmentPrice, MATH_CONTEXT);
            LOG.info("Calculating sandbox 2020 owning cost, compute dominates, hdd segment cost = {}, " +
                    "ssd segment cost = {}, hdd share = {}, ssd share = {}", hddSegmentCost, ssdSegmentCost,
                    hddShare, ssdShare);
            if (hasCpu) {
                result.put(changes.get(Resource2020.CPU_SEGMENTED).getChange().getKey(),
                        hddSegmentCost.add(ssdSegmentCost, MATH_CONTEXT));
                changes.forEach((resource, change) -> {
                    if (resource != Resource2020.CPU_SEGMENTED) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            } else {
                if (hasRam) {
                    result.put(changes.get(Resource2020.RAM_SEGMENTED).getChange().getKey(),
                            hddSegmentCost.add(ssdSegmentCost, MATH_CONTEXT));
                }
                changes.forEach((resource, change) -> {
                    if (resource != Resource2020.RAM_SEGMENTED) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            }
            LOG.info("Calculating sandbox 2020 owning cost, compute dominates, result = {}", result);
        }
        return result;
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculate2021CampaignOwningCost(
            List<ChangeOwningCostContext> changes, Map<String, ? extends PricingModel> pricingBySKU) {
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        Map<Long, List<ChangeOwningCostContext>> changesByBigOrder = new HashMap<>();
        changes.forEach(change -> {
            if (change.getChange().getBigOrder() != null) {
                changesByBigOrder.computeIfAbsent(change.getChange().getBigOrder().getId(), k -> new ArrayList<>())
                        .add(change);
            } else {
                result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
            }
        });
        changesByBigOrder.forEach((bigOrderId, bigOrderChanges) -> {
            Map<QuotaChangeRequest.ChangeKey, BigDecimal> bigOrderCosts = calculate2021CampaignOrderOwningCost(
                    bigOrderChanges, pricingBySKU);
            LOG.info("Calculated sandbox owning cost for 2021 big order {}: {}", bigOrderId, bigOrderCosts);
            result.putAll(bigOrderCosts);
        });
        return result;
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculate2021CampaignOrderOwningCost(
            List<ChangeOwningCostContext> changes, Map<String, ? extends PricingModel> pricingBySKU) {
        LOG.info("Calculating sandbox owning cost in 2021 big order for {}", changes);
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        Map<Resource2021, ChangeOwningCostContext> linuxChanges = new HashMap<>();
        changes.forEach(change -> {
            String resourceKey = change.getChange().getResource().getPublicKey();
            Optional<Resource2021> resourceO = Resource2021.byKey(resourceKey);
            if (resourceO.isEmpty()) {
                result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                return;
            }
            switch (resourceO.get()) {
                case CPU_LINUX:
                case RAM_LINUX:
                case SSD_LINUX:
                case HDD_LINUX:
                    linuxChanges.put(resourceO.get(), change);
                    break;
                case S3_STORAGE:
                    result.put(change.getChange().getKey(), calculateForS3Storage2021(change.getChange()));
                    break;
                case MAC_MINI:
                    result.put(change.getChange().getKey(), calculateForMacMini2021(change.getChange(), pricingBySKU));
                    break;
                case WINDOWS:
                    result.put(change.getChange().getKey(), calculateForWindows2021(change.getChange(), pricingBySKU));
                    break;
                default:
                    result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
            }
        });
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> linuxCosts = calculate2021CampaignOrderLinuxSegmentOwningCost(
                linuxChanges, pricingBySKU);
        LOG.info("Calculated sandbox owning cost for linux 2021 big order: {}", linuxCosts);
        result.putAll(linuxCosts);
        return result;
    }

    private Map<QuotaChangeRequest.ChangeKey, BigDecimal> calculate2021CampaignOrderLinuxSegmentOwningCost(
            Map<Resource2021, ChangeOwningCostContext> changes, Map<String, ? extends PricingModel> pricingBySKU) {
        Map<QuotaChangeRequest.ChangeKey, BigDecimal> result = new HashMap<>();
        if (!pricingBySKU.containsKey(SLOT_GUARANTEE_SSD_SKU) ||
                !pricingBySKU.containsKey(SLOT_USAGE_SSD_SKU) ||
                !pricingBySKU.containsKey(SLOT_GUARANTEE_HDD_SKU) ||
                !pricingBySKU.containsKey(SLOT_USAGE_HDD_SKU)) {
            changes.forEach((resource, change) -> result.put(change.getChange().getKey(), DEFAULT_OWNING_COST));
            LOG.info("Calculating sandbox 2021 owning cost, no prices, result = {}", result);
            return result;
        }
        BigDecimal ssdSegmentPrice = pricingBySKU.get(SLOT_GUARANTEE_SSD_SKU).getPrice()
                .add(pricingBySKU.get(SLOT_USAGE_SSD_SKU).getPrice(), MATH_CONTEXT);
        BigDecimal hddSegmentPrice = pricingBySKU.get(SLOT_GUARANTEE_HDD_SKU).getPrice()
                .add(pricingBySKU.get(SLOT_USAGE_HDD_SKU).getPrice(), MATH_CONTEXT);
        boolean hasCpu = changes.containsKey(Resource2021.CPU_LINUX);
        boolean hasRam = changes.containsKey(Resource2021.RAM_LINUX);
        boolean hasSsd = changes.containsKey(Resource2021.SSD_LINUX);
        boolean hasHdd = changes.containsKey(Resource2021.HDD_LINUX);
        BigDecimal cpuCores = Optional.ofNullable(changes.get(Resource2021.CPU_LINUX))
                .map(v -> convert(v.getChange(), DiUnit.CORES, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal ramBytes = Optional.ofNullable(changes.get(Resource2021.RAM_LINUX))
                .map(v -> convert(v.getChange(), DiUnit.BYTE, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal hddBytes = Optional.ofNullable(changes.get(Resource2021.HDD_LINUX))
                .map(v -> convert(v.getChange(), DiUnit.BYTE, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal ssdBytes = Optional.ofNullable(changes.get(Resource2021.SSD_LINUX))
                .map(v -> convert(v.getChange(), DiUnit.BYTE, LOG)).orElse(BigDecimal.ZERO);
        BigDecimal coresForRamBytes = ramBytes.divide(RAM_BYTES_PER_LINUX_CORE, MATH_CONTEXT)
                .setScale(0, RoundingMode.UP);
        BigDecimal coresForHddBytes = hddBytes.divide(HDD_BYTES_PER_LINUX_CORE, MATH_CONTEXT)
                .setScale(0, RoundingMode.UP);
        BigDecimal coresForSsdBytes = ssdBytes.divide(SSD_BYTES_PER_LINUX_CORE, MATH_CONTEXT)
                .setScale(0, RoundingMode.UP);
        BigDecimal storageCores = coresForHddBytes.add(coresForSsdBytes, MATH_CONTEXT);
        BigDecimal computeCores = cpuCores.max(coresForRamBytes);
        LOG.info("Calculating sandbox 2021 owning cost, cpu cores = {}, cores for ram bytes = {}, cores for hdd bytes = {}, " +
                "cores for ssd bytes = {}, storage cores = {}, compute cores = {}", cpuCores, coresForRamBytes, coresForHddBytes,
                coresForSsdBytes, storageCores, computeCores);
        if (storageCores.compareTo(computeCores) >= 0) {
            BigDecimal hddSegmentCost = coresForHddBytes.multiply(hddSegmentPrice, MATH_CONTEXT);
            BigDecimal ssdSegmentCost = coresForSsdBytes.multiply(ssdSegmentPrice, MATH_CONTEXT);
            LOG.info("Calculating sandbox 2021 owning cost, storage dominates, hdd segment cost = {}, " +
                    "ssd segment cost = {}", hddSegmentCost, ssdSegmentCost);
            if (hasCpu) {
                result.put(changes.get(Resource2021.CPU_LINUX).getChange().getKey(),
                        hddSegmentCost.add(ssdSegmentCost, MATH_CONTEXT));
                changes.forEach((resource, change) -> {
                    if (resource != Resource2021.CPU_LINUX) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            } else {
                if (hasHdd) {
                    result.put(changes.get(Resource2021.HDD_LINUX).getChange().getKey(), hddSegmentCost);
                }
                if (hasSsd) {
                    result.put(changes.get(Resource2021.SSD_LINUX).getChange().getKey(), ssdSegmentCost);
                }
                changes.forEach((resource, change) -> {
                    if (resource != Resource2021.HDD_LINUX && resource != Resource2021.SSD_LINUX) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            }
            LOG.info("Calculating sandbox 2021 owning cost, storage dominates, result = {}", result);
        } else {
            BigDecimal hddShare;
            BigDecimal ssdShare;
            if (coresForHddBytes.compareTo(BigDecimal.ZERO) == 0 && coresForSsdBytes.compareTo(BigDecimal.ZERO) == 0) {
                hddShare = new BigDecimal("0.0", MATH_CONTEXT);
                ssdShare = new BigDecimal("1.0", MATH_CONTEXT);
            } else {
                hddShare = coresForHddBytes.divide(coresForHddBytes.add(coresForSsdBytes, MATH_CONTEXT), MATH_CONTEXT);
                ssdShare = coresForSsdBytes.divide(coresForHddBytes.add(coresForSsdBytes, MATH_CONTEXT), MATH_CONTEXT);
            }
            BigDecimal hddSegmentCost = computeCores.multiply(hddShare, MATH_CONTEXT)
                    .multiply(hddSegmentPrice, MATH_CONTEXT);
            BigDecimal ssdSegmentCost = computeCores.multiply(ssdShare, MATH_CONTEXT)
                    .multiply(ssdSegmentPrice, MATH_CONTEXT);
            LOG.info("Calculating sandbox 2021 owning cost, compute dominates, hdd segment cost = {}, " +
                    "ssd segment cost = {}, hdd share = {}, ssd share = {}", hddSegmentCost, ssdSegmentCost,
                    hddShare, ssdShare);
            if (hasCpu) {
                result.put(changes.get(Resource2021.CPU_LINUX).getChange().getKey(),
                        hddSegmentCost.add(ssdSegmentCost, MATH_CONTEXT));
                changes.forEach((resource, change) -> {
                    if (resource != Resource2021.CPU_LINUX) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            } else {
                if (hasRam) {
                    result.put(changes.get(Resource2021.RAM_LINUX).getChange().getKey(),
                            hddSegmentCost.add(ssdSegmentCost, MATH_CONTEXT));
                }
                changes.forEach((resource, change) -> {
                    if (resource != Resource2021.RAM_LINUX) {
                        result.put(change.getChange().getKey(), DEFAULT_OWNING_COST);
                    }
                });
            }
            LOG.info("Calculating sandbox 2021 owning cost, compute dominates, result = {}", result);
        }
        return result;
    }

    private BigDecimal calculateForS3Storage2021(QuotaChangeRequest.Change change) {
        return convert(change, DiUnit.GIBIBYTE, LOG)
                .multiply(S3_STORAGE_TARIFF_BY_GIB, MATH_CONTEXT);
    }

    private BigDecimal calculateForMacMini2021(QuotaChangeRequest.Change change,
                                               Map<String, ? extends PricingModel> pricingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (pricingModelsBySKU.containsKey(SLOT_GUARANTEE_MACOS_SKU) &&
                pricingModelsBySKU.containsKey(SLOT_USAGE_MACOS_SKU)) {
            PricingModel guaranteePricingModel = pricingModelsBySKU.get(SLOT_GUARANTEE_MACOS_SKU);
            PricingModel usagePricingModel = pricingModelsBySKU.get(SLOT_USAGE_MACOS_SKU);
            BigDecimal totalPrice = guaranteePricingModel.getPrice().add(usagePricingModel.getPrice(), MATH_CONTEXT);
            result = convert(change, DiUnit.COUNT, LOG)
                    .multiply(CORES_PER_MAC_MINI, MATH_CONTEXT)
                    .multiply(totalPrice, MATH_CONTEXT);
        }
        LOG.info("Calculated sandbox owning cost for mac-mini 2021 big order {}: {}", change, result);
        return result;
    }

    private BigDecimal calculateForWindows2021(QuotaChangeRequest.Change change,
                                               Map<String, ? extends PricingModel> pricingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (pricingModelsBySKU.containsKey(SLOT_GUARANTEE_WINDOWS_SKU) &&
                pricingModelsBySKU.containsKey(SLOT_USAGE_WINDOWS_SKU)) {
            PricingModel guaranteePricingModel = pricingModelsBySKU.get(SLOT_GUARANTEE_WINDOWS_SKU);
            PricingModel usagePricingModel = pricingModelsBySKU.get(SLOT_USAGE_WINDOWS_SKU);
            BigDecimal totalPrice = guaranteePricingModel.getPrice().add(usagePricingModel.getPrice(), MATH_CONTEXT);
            result = convert(change, DiUnit.COUNT, LOG)
                    .multiply(CORES_PER_WINDOWS, MATH_CONTEXT)
                    .multiply(totalPrice, MATH_CONTEXT);
        }
        LOG.info("Calculated sandbox owning cost for windows 2021 big order {}: {}", change, result);
        return result;
    }

    private BigDecimal calculateForMacMini2020(QuotaChangeRequest.Change change,
                                               Map<String, ? extends PricingModel> pricingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (pricingModelsBySKU.containsKey(SLOT_GUARANTEE_MACOS_SKU) &&
                pricingModelsBySKU.containsKey(SLOT_USAGE_MACOS_SKU)) {
            PricingModel guaranteePricingModel = pricingModelsBySKU.get(SLOT_GUARANTEE_MACOS_SKU);
            PricingModel usagePricingModel = pricingModelsBySKU.get(SLOT_USAGE_MACOS_SKU);
            BigDecimal totalPrice = guaranteePricingModel.getPrice().add(usagePricingModel.getPrice(), MATH_CONTEXT);
            result = convert(change, DiUnit.CORES, LOG)
                    .divide(CORES_PER_MAC_MINI, MATH_CONTEXT).setScale(0, RoundingMode.UP)
                    .multiply(CORES_PER_MAC_MINI, MATH_CONTEXT)
                    .multiply(totalPrice, MATH_CONTEXT);
        }
        LOG.info("Calculated sandbox owning cost for mac-mini 2020 big order {}: {}", change, result);
        return result;
    }

    private BigDecimal calculateForWindows2020(QuotaChangeRequest.Change change,
                                               Map<String, ? extends PricingModel> pricingModelsBySKU) {
        BigDecimal result = DEFAULT_OWNING_COST;
        if (pricingModelsBySKU.containsKey(SLOT_GUARANTEE_WINDOWS_SKU) &&
                pricingModelsBySKU.containsKey(SLOT_USAGE_WINDOWS_SKU)) {
            PricingModel guaranteePricingModel = pricingModelsBySKU.get(SLOT_GUARANTEE_WINDOWS_SKU);
            PricingModel usagePricingModel = pricingModelsBySKU.get(SLOT_USAGE_WINDOWS_SKU);
            BigDecimal totalPrice = guaranteePricingModel.getPrice().add(usagePricingModel.getPrice(), MATH_CONTEXT);
            result = convert(change, DiUnit.CORES, LOG)
                    .divide(CORES_PER_WINDOWS, MATH_CONTEXT).setScale(0, RoundingMode.UP)
                    .multiply(CORES_PER_WINDOWS, MATH_CONTEXT)
                    .multiply(totalPrice, MATH_CONTEXT);
        }
        LOG.info("Calculated sandbox owning cost for windows 2020 big order {}: {}", change, result);
        return result;
    }

    private Optional<SandboxSegment2020> getSegmentFromChange(QuotaChangeRequest.Change change) {
        return getClusterSegment(change)
                .flatMap(segment -> SandboxSegment2020.byKey(segment.getPublicKey()));
    }

    private Optional<Segment> getClusterSegment(final QuotaChangeRequest.Change change) {
        return SegmentUtils.getSegmentBySegmentationKey(change.getSegments(), sandboxOldSegmentationKey);
    }

    private enum Resource2020 implements EnumUtils.StringKey {
        CPU_SEGMENTED("cpu_segmented"),
        RAM_SEGMENTED("ram_segmented"),
        SSD_SEGMENTED("ssd_segmented"),
        HDD_SEGMENTED("hdd_segmented"),
        ;

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

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

        public String getKey() {
            return key;
        }

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

    }

    private enum Resource2022 implements EnumUtils.StringKey {
        CPU_LINUX_HDD("cpu_linux_hdd"),
        CPU_LINUX_SSD("cpu_linux_ssd"),
        CPU_MACOS("cpu_macos"),
        CPU_WINDOWS("cpu_windows"),
        S3_HDD_X2("s3_hdd_x2"),
        ;

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

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

        public String getKey() {
            return key;
        }

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

    }

    private enum Resource2021 implements EnumUtils.StringKey {
        CPU_LINUX("cpu_linux"),
        RAM_LINUX("ram_linux"),
        SSD_LINUX("ssd_linux"),
        HDD_LINUX("hdd_linux"),
        S3_STORAGE("s3_storage"),
        MAC_MINI("mac_mini"),
        WINDOWS("windows"),
        ;

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

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

        public String getKey() {
            return key;
        }

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

    }

    private enum SandboxSegment2020 implements EnumUtils.StringKey {
        SANDBOX_LINUX_BARE_METAL("sandbox_linux_bare_metal"),
        SANDBOX_LINUX_YP("sandbox_linux_yp"),
        SANDBOX_WINDOWS("sandbox_windows"),
        SANDBOX_MAC_MINI("sandbox_mac_mini"),
        ;

        private static Map<String, SandboxSegment2020> segmentByKey;
        private final String key;

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

        @Override
        public String getKey() {
            return key;
        }

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

    }

}
