package ru.yandex.dispenser.validation.providers.yt;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import ru.yandex.dispenser.validation.client.CampaignType;
import ru.yandex.dispenser.validation.client.Environment;
import ru.yandex.dispenser.validation.client.ErrorCollection;
import ru.yandex.dispenser.validation.client.OrdersSubset;
import ru.yandex.dispenser.validation.client.Result;
import ru.yandex.dispenser.validation.client.model.Amount;
import ru.yandex.dispenser.validation.client.model.BaseResourceChange;
import ru.yandex.dispenser.validation.client.model.CreateChange;
import ru.yandex.dispenser.validation.client.model.CreateQuotaRequest;
import ru.yandex.dispenser.validation.client.model.QuotaRequest;
import ru.yandex.dispenser.validation.client.model.Units;
import ru.yandex.dispenser.validation.providers.CommonSettings;
import ru.yandex.dispenser.validation.providers.ProviderValidator;
import ru.yandex.dispenser.validation.providers.RequestedAmount;
import ru.yandex.dispenser.validation.providers.RequestedResource;
import ru.yandex.dispenser.validation.providers.SegmentedResourceKey;

/**
 * YT validator.
 *
 * @author Dmitriy Timashov <dm-tim@yandex-team.ru>
 */
@Component
public class YtValidator implements ProviderValidator {

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

    private static final String YT_PROVIDER_KEY = "yt";
    private static final String PROXY_PROVIDER_KEY = "logbroker_yt_proxy";

    private final CommonSettings commonSettings;
    private final Map<String, List<RequestedResource>> ytResources;
    private final Map<String, List<RequestedResource>> proxyResources;
    private final Set<String> reducedOrderSubsetSegmentKeys;

    public YtValidator(CommonSettings commonSettings) {
        this.commonSettings = commonSettings;
        List<RequestedResource> allResourcesWithoutGpu = List.of(
                new RequestedResource("cpu", Units.Cores.CORES),
                new RequestedResource("hdd", Units.Bytes.GIBIBYTES),
                new RequestedResource("ssd", Units.Bytes.GIBIBYTES),
                new RequestedResource("cpu_dynt", Units.Cores.CORES),
                new RequestedResource("mem_dynt", Units.Bytes.GIBIBYTES),
                new RequestedResource("burst_guarantee_cpu", Units.Cores.CORES),
                new RequestedResource("cpu_flow", Units.Cores.CORES),
                new RequestedResource("mem_strong", Units.Bytes.GIBIBYTES),
                new RequestedResource("mem_relaxed", Units.Bytes.GIBIBYTES),
                new RequestedResource("mem_burst", Units.Bytes.GIBIBYTES));
        List<RequestedResource> dynTablesResources = List.of(
                new RequestedResource("hdd", Units.Bytes.GIBIBYTES),
                new RequestedResource("ssd", Units.Bytes.GIBIBYTES),
                new RequestedResource("cpu_dynt", Units.Cores.CORES),
                new RequestedResource("mem_dynt", Units.Bytes.GIBIBYTES));
        List<RequestedResource> dynTablesAndComputeResources = List.of(
                new RequestedResource("hdd", Units.Bytes.GIBIBYTES),
                new RequestedResource("ssd", Units.Bytes.GIBIBYTES),
                new RequestedResource("cpu", Units.Cores.CORES),
                new RequestedResource("mem_strong", Units.Bytes.GIBIBYTES),
                new RequestedResource("cpu_dynt", Units.Cores.CORES),
                new RequestedResource("mem_dynt", Units.Bytes.GIBIBYTES));
        List<RequestedResource> storageResources = List.of(
                new RequestedResource("hdd", Units.Bytes.GIBIBYTES),
                new RequestedResource("ssd", Units.Bytes.GIBIBYTES));
        this.ytResources = new HashMap<>();
        this.ytResources.put("hahn", allResourcesWithoutGpu);
        this.ytResources.put("arnold", allResourcesWithoutGpu);
        this.ytResources.put("seneca-sas", dynTablesResources);
        this.ytResources.put("seneca-vla", dynTablesResources);
        this.ytResources.put("seneca-klg", dynTablesResources);
        this.ytResources.put("freud", allResourcesWithoutGpu);
        this.ytResources.put("hume", allResourcesWithoutGpu);
        this.ytResources.put("landau", allResourcesWithoutGpu);
        this.ytResources.put("bohr", allResourcesWithoutGpu);
        this.ytResources.put("zeno", dynTablesResources);
        this.ytResources.put("locke", storageResources);
        this.ytResources.put("markov", dynTablesResources);
        this.ytResources.put("vanga", dynTablesAndComputeResources);
        this.ytResources.put("pythia", dynTablesAndComputeResources);
        this.ytResources.put("nash", dynTablesResources);
        this.proxyResources = Map.of("hahn", List.of(new RequestedResource("data_flow_binary",
                        Units.BinaryBytesPerSecond.MEBIBYTES_PER_SECOND)), "arnold",
                List.of(new RequestedResource("data_flow_binary", Units.BinaryBytesPerSecond
                        .MEBIBYTES_PER_SECOND)));
        this.reducedOrderSubsetSegmentKeys = Set.of("seneca-klg");
    }

    @Override
    public CreateQuotaRequest prepareTestRequest(Environment environment, CampaignType campaignType) {
        CreateQuotaRequest.Builder builder = commonSettings.baseQuotaRequest(providerName());
        List<Long> orders = commonSettings.orders(campaignType, OrdersSubset.FULL);
        List<Long> ordersReducedSubset = commonSettings.orders(campaignType, OrdersSubset.REDUCED);
        long currentValue = 1L;
        for (Map.Entry<String, List<RequestedResource>> entry : ytResources.entrySet()) {
            String clusterKey = entry.getKey();
            List<RequestedResource> resources = entry.getValue();
            for (RequestedResource resource : resources) {
                List<Long> selectedOrders = reducedOrderSubsetSegmentKeys.contains(clusterKey)
                        ? ordersReducedSubset : orders;
                for (Long orderId : selectedOrders) {
                    long value = currentValue++;
                    builder.addChange(CreateChange.builder()
                            .serviceKey(YT_PROVIDER_KEY)
                            .orderId(orderId)
                            .resourceKey(resource.getKey())
                            .addSegmentKey(clusterKey)
                            .amount(Amount.builder()
                                    .value(value)
                                    .unit(resource.getUnit())
                                    .build())
                            .build());
                }
            }
        }
        for (Map.Entry<String, List<RequestedResource>> entry : proxyResources.entrySet()) {
            String clusterKey = entry.getKey();
            List<RequestedResource> resources = entry.getValue();
            for (RequestedResource resource : resources) {
                for (Long orderId : orders) {
                    long value = currentValue++;
                    builder.addChange(CreateChange.builder()
                            .serviceKey(PROXY_PROVIDER_KEY)
                            .orderId(orderId)
                            .resourceKey(resource.getKey())
                            .addSegmentKey(clusterKey)
                            .amount(Amount.builder()
                                    .value(value)
                                    .unit(resource.getUnit())
                                    .build())
                            .build());
                }
            }
        }
        return builder.build();
    }

    @Override
    @SuppressWarnings("MethodLength")
    public Result<String> validateTestRequest(QuotaRequest request, Environment environment,
                                              CampaignType campaignType) {
        Map<Long, Map<SegmentedResourceKey, RequestedAmount>> requestedResources = new HashMap<>();
        request.getChanges().forEach(change -> {
            SegmentedResourceKey key = new SegmentedResourceKey(change.getService().getKey(),
                    change.getResource().getKey(), change.getSegmentKeys());
            RequestedAmount amount = new RequestedAmount(change.getAmount().getValue(),
                    Units.UNITS.get(change.getAmount().getUnit()));
            requestedResources.computeIfAbsent(change.getOrder().getId(), k -> new HashMap<>()).put(key, amount);
        });
        Map<Long, Map<SegmentedResourceKey, RequestedAmount>> baseResources = new HashMap<>();
        Objects.requireNonNullElse(request.getBaseResourceChanges(), Set.<BaseResourceChange>of()).forEach(change -> {
            SegmentedResourceKey key = new SegmentedResourceKey(change.getBaseResource().getType()
                    .getProvider().getKey(), change.getBaseResource().getType().getKey(),
                    change.getBaseResource().getSegmentKeys());
            RequestedAmount amount = new RequestedAmount(change.getTotalAmount().getValue(),
                    Units.UNITS.get(change.getTotalAmount().getUnit()));
            baseResources.computeIfAbsent(change.getBigOrder().getId(), k -> new HashMap<>()).put(key, amount);
        });
        Map<Long, Map<SegmentedResourceKey, RequestedAmount>> expectedBaseResources = new HashMap<>();
        for (Map.Entry<Long, Map<SegmentedResourceKey, RequestedAmount>> entry : requestedResources.entrySet()) {
            Long orderId = entry.getKey();
            Map<SegmentedResourceKey, RequestedAmount> resources = entry.getValue();
            for (String clusterKey : ytResources.keySet()) {
                RequestedAmount requestedYtCpu = resources.getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY,
                        "cpu", Set.of(clusterKey)), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                RequestedAmount requestedYtHdd = resources.getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY,
                        "hdd", Set.of(clusterKey)), new RequestedAmount(0, Units.Bytes.BYTES));
                RequestedAmount requestedYtSsd = resources.getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY,
                        "ssd", Set.of(clusterKey)), new RequestedAmount(0, Units.Bytes.BYTES));
                RequestedAmount requestedYtCpuDynTables = resources.getOrDefault(new SegmentedResourceKey(
                        YT_PROVIDER_KEY, "cpu_dynt", Set.of(clusterKey)),
                        new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                RequestedAmount requestedYtMemDynTables = resources.getOrDefault(new SegmentedResourceKey(
                        YT_PROVIDER_KEY, "mem_dynt", Set.of(clusterKey)),
                        new RequestedAmount(0, Units.Bytes.BYTES));
                RequestedAmount requestedYtBurstGuaranteeCpu = resources.getOrDefault(new SegmentedResourceKey(
                        YT_PROVIDER_KEY, "burst_guarantee_cpu", Set.of(clusterKey)),
                        new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                RequestedAmount requestedYtCpuFlow = resources.getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY,
                        "cpu_flow", Set.of(clusterKey)), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                RequestedAmount requestedYtMemStrong = resources.getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY,
                        "mem_strong", Set.of(clusterKey)), new RequestedAmount(0, Units.Bytes.BYTES));
                RequestedAmount requestedYtMemRelaxed = resources.getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY,
                        "mem_relaxed", Set.of(clusterKey)), new RequestedAmount(0, Units.Bytes.BYTES));
                RequestedAmount requestedYtMemBurst = resources.getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY,
                        "mem_burst", Set.of(clusterKey)), new RequestedAmount(0, Units.Bytes.BYTES));
                if (!requestedYtCpuFlow.getUnit().equals(Units.Cores.PERMILLE_CORES)) {
                    LOG.error("Unexpected YT CPU flow unit: {}", requestedYtCpuFlow);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT CPU flow unit").build());
                }
                if (!requestedYtCpu.getUnit().equals(Units.Cores.PERMILLE_CORES)) {
                    LOG.error("Unexpected YT CPU unit: {}", requestedYtCpu);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT CPU unit").build());
                }
                if (!requestedYtCpuDynTables.getUnit().equals(Units.Cores.PERMILLE_CORES)) {
                    LOG.error("Unexpected YT CPU dynamic tables unit: {}", requestedYtCpuDynTables);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT CPU dynamic tables unit")
                            .build());
                }
                long cpuDayAmount = requestedYtCpuFlow.getAmount() +
                        requestedYtCpu.getAmount() + requestedYtCpuDynTables.getAmount();
                if (cpuDayAmount > 0L) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:cpu_day",
                                    Set.of(clusterKey)), new RequestedAmount(cpuDayAmount,
                                    Units.Cores.PERMILLE_CORES));
                }
                if (!requestedYtBurstGuaranteeCpu.getUnit().equals(Units.Cores.PERMILLE_CORES)) {
                    LOG.error("Unexpected YT burst guarantee CPU unit: {}", requestedYtBurstGuaranteeCpu);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT burst guarantee CPU unit")
                            .build());
                }
                long burstCpuAmount = requestedYtBurstGuaranteeCpu.getAmount() +
                        requestedYtCpu.getAmount() + requestedYtCpuDynTables.getAmount();
                if (burstCpuAmount > 0L) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:burst_cpu",
                                    Set.of(clusterKey)), new RequestedAmount(burstCpuAmount,
                                    Units.Cores.PERMILLE_CORES));
                }
                if (!requestedYtMemStrong.getUnit().equals(Units.Bytes.BYTES)) {
                    LOG.error("Unexpected YT mem strong unit: {}", requestedYtMemStrong);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT mem strong unit").build());
                }
                if (!requestedYtMemRelaxed.getUnit().equals(Units.Bytes.BYTES)) {
                    LOG.error("Unexpected YT mem relaxed unit: {}", requestedYtMemRelaxed);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT mem relaxed unit").build());
                }
                if (!requestedYtMemDynTables.getUnit().equals(Units.Bytes.BYTES)) {
                    LOG.error("Unexpected YT dynamic tables memory unit: {}", requestedYtMemDynTables);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT dynamic tables memory unit")
                            .build());
                }
                long ramDayAmount = toGibibytes(requestedYtMemStrong.getAmount())
                        + toGibibytes(requestedYtMemRelaxed.getAmount())
                        + toGibibytes(requestedYtMemDynTables.getAmount());
                if (ramDayAmount > 0L) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:ram_day",
                                    Set.of(clusterKey)), new RequestedAmount(ramDayAmount,
                                    Units.Gibibytes.GIBIBYTES));
                }
                if (!requestedYtMemBurst.getUnit().equals(Units.Bytes.BYTES)) {
                    LOG.error("Unexpected YT mem burst unit: {}", requestedYtMemBurst);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT mem burst unit").build());
                }
                long burstRamAmount = toGibibytes(requestedYtMemStrong.getAmount())
                        + toGibibytes(requestedYtMemBurst.getAmount())
                        + toGibibytes(requestedYtMemDynTables.getAmount());
                if (burstRamAmount > 0L) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:burst_ram",
                                    Set.of(clusterKey)), new RequestedAmount(burstRamAmount,
                                    Units.Gibibytes.GIBIBYTES));
                }
                if (!requestedYtHdd.getUnit().equals(Units.Bytes.BYTES)) {
                    LOG.error("Unexpected YT hdd unit: {}", requestedYtHdd);
                    return Result.failure(ErrorCollection.builder().addError("Unexpected YT hdd unit").build());
                }
                long hddAmount = toGibibytes(requestedYtHdd.getAmount());
                if (hddAmount > 0L) {
                    if ("markov".equals(clusterKey)) {
                        RequestedAmount currentSsd = expectedBaseResources.getOrDefault(orderId, Map.of())
                                .getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:ssd",
                                        Set.of(clusterKey)), new RequestedAmount(0, Units.Gibibytes.GIBIBYTES));
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:ssd",
                                        Set.of(clusterKey)), new RequestedAmount(currentSsd.getAmount() + hddAmount,
                                        Units.Gibibytes.GIBIBYTES));
                    } else {
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:hdd",
                                        Set.of(clusterKey)), new RequestedAmount(hddAmount,
                                        Units.Gibibytes.GIBIBYTES));
                    }
                }
                long ssdAmount = toGibibytes(requestedYtSsd.getAmount());
                if (ssdAmount > 0L) {
                    RequestedAmount currentSsd = expectedBaseResources.getOrDefault(orderId, Map.of())
                            .getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:ssd",
                                    Set.of(clusterKey)), new RequestedAmount(0, Units.Gibibytes.GIBIBYTES));
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:ssd",
                                    Set.of(clusterKey)), new RequestedAmount(currentSsd.getAmount() + ssdAmount,
                                    Units.Gibibytes.GIBIBYTES));
                }
            }
            RequestedAmount currentCpuDayHahn = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                    new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:cpu_day",
                            Set.of("hahn")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
            RequestedAmount currentBurstCpuHahn = expectedBaseResources.getOrDefault(orderId, Map.of())
                    .getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:burst_cpu",
                            Set.of("hahn")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
            RequestedAmount currentCpuDayArnold = expectedBaseResources.getOrDefault(orderId, Map.of())
                    .getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:cpu_day",
                            Set.of("arnold")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
            RequestedAmount currentBurstCpuArnold = expectedBaseResources.getOrDefault(orderId, Map.of())
                    .getOrDefault(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:burst_cpu",
                            Set.of("arnold")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
            RequestedAmount requestedProxyDataFlowHahn = resources.getOrDefault(new SegmentedResourceKey(
                            PROXY_PROVIDER_KEY, "data_flow_binary", Set.of("hahn")),
                    new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
            RequestedAmount requestedProxyDataFlowArnold = resources.getOrDefault(new SegmentedResourceKey(
                            PROXY_PROVIDER_KEY, "data_flow_binary", Set.of("arnold")),
                    new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
            if (!requestedProxyDataFlowHahn.getUnit().equals(Units.BinaryBytesPerSecond.BINARY_BYTES_PER_SECOND)) {
                LOG.error("Unexpected Proxy Hahn dataflow unit: {}", requestedProxyDataFlowHahn);
                return Result.failure(ErrorCollection.builder().addError("Unexpected Proxy Hahn dataflow unit")
                        .build());
            }
            if (!requestedProxyDataFlowArnold.getUnit().equals(Units.BinaryBytesPerSecond.BINARY_BYTES_PER_SECOND)) {
                LOG.error("Unexpected Proxy Arnold dataflow unit: {}", requestedProxyDataFlowArnold);
                return Result.failure(ErrorCollection.builder().addError("Unexpected Proxy Arnold dataflow unit")
                        .build());
            }
            // Rounding up
            long cpuDayAmountHahnProxy = (long) Math.ceil((13.0d * 2.0d *
                    requestedProxyDataFlowHahn.getAmount()) / 190000.0d);
            long burstCpuAmountHahnProxy = (long) Math.ceil((13.0d * 2.0d *
                    requestedProxyDataFlowHahn.getAmount()) / 190000.0d);
            long cpuDayAmountArnoldProxy = (long) Math.ceil((13.0d * 2.0d *
                    requestedProxyDataFlowArnold.getAmount()) / 190000.0d);
            long burstCpuAmountArnoldProxy = (long) Math.ceil((13.0d * 2.0d *
                    requestedProxyDataFlowArnold.getAmount()) / 190000.0d);
            if (cpuDayAmountHahnProxy > 0) {
                expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                        .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:cpu_day",
                                Set.of("hahn")), new RequestedAmount(currentCpuDayHahn.getAmount()
                                + cpuDayAmountHahnProxy, Units.Cores.PERMILLE_CORES));
            }
            if (burstCpuAmountHahnProxy > 0) {
                expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                        .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:burst_cpu",
                                Set.of("hahn")), new RequestedAmount(currentBurstCpuHahn.getAmount()
                                + burstCpuAmountHahnProxy, Units.Cores.PERMILLE_CORES));
            }

            if (cpuDayAmountArnoldProxy > 0) {
                expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                        .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:cpu_day",
                                Set.of("arnold")), new RequestedAmount(currentCpuDayArnold.getAmount()
                                + cpuDayAmountArnoldProxy, Units.Cores.PERMILLE_CORES));
            }
            if (burstCpuAmountArnoldProxy > 0) {
                expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                        .put(new SegmentedResourceKey(YT_PROVIDER_KEY, "yt:burst_cpu",
                                Set.of("arnold")), new RequestedAmount(currentBurstCpuArnold.getAmount()
                                + burstCpuAmountArnoldProxy, Units.Cores.PERMILLE_CORES));
            }
        }
        LOG.info("Expected {} base resources: {}", providerName(), expectedBaseResources);
        LOG.info("Actual {} base resources: {}", providerName(), baseResources);
        boolean match = baseResources.equals(expectedBaseResources);
        if (match) {
            return Result.success(providerName());
        } else {
            baseResources.forEach((orderId, resources) ->
                    resources.forEach((key, amount) -> {
                        RequestedAmount expectedAmount = expectedBaseResources.getOrDefault(orderId, Map.of()).get(key);
                        if (!Objects.equals(amount, expectedAmount)) {
                            LOG.error("Mismatch for {} {}: expected {}, actual {}", orderId, key, expectedAmount,
                                    amount);
                        }
                    }));
            expectedBaseResources.forEach((orderId, resources) ->
                    resources.forEach((key, expectedAmount) -> {
                        RequestedAmount amount = baseResources.getOrDefault(orderId, Map.of()).get(key);
                        if (!Objects.equals(amount, expectedAmount)) {
                            LOG.error("Mismatch for {} {}: expected {}, actual {}", orderId, key, expectedAmount,
                                    amount);
                        }
                    }));
            return Result.failure(ErrorCollection.builder().addError("Base resource mismatch for " + providerName())
                    .build());
        }
    }

    @Override
    public String providerName() {
        return "YT";
    }

    private long toGibibytes(long bytes) {
        return bytes / (1024L * 1024L * 1024L);
    }

}
