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

import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.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;

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

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

    private static final String YP_PROVIDER_KEY = "yp";
    private static final String SAAS_PROVIDER_KEY = "saas";

    private final CommonSettings commonSettings;
    private final List<RequestedResource> ypResources;
    private final List<RequestedResource> saasResources;
    private final List<Set<String>> ypSegments;
    private final Set<String> saasSegments;
    private final Set<String> reducedOrderSubsetSegmentKeys;

    public YpValidator(CommonSettings commonSettings) {
        this.commonSettings = commonSettings;
        this.ypResources = List.of(new RequestedResource("cpu_segmented", Units.Cores.CORES),
                new RequestedResource("ssd_segmented", Units.Bytes.GIBIBYTES),
                new RequestedResource("hdd_segmented", Units.Bytes.GIBIBYTES),
                new RequestedResource("ram_segmented", Units.Bytes.GIBIBYTES),
                new RequestedResource("io_hdd", Units.BinaryBytesPerSecond.MEBIBYTES_PER_SECOND),
                new RequestedResource("io_ssd", Units.BinaryBytesPerSecond.MEBIBYTES_PER_SECOND),
                new RequestedResource("io_net", Units.BinaryBytesPerSecond.MEBIBYTES_PER_SECOND)
                );
        this.saasResources = List.of(new RequestedResource("cpu", Units.Cores.CORES),
                new RequestedResource("ram", Units.Bytes.GIBIBYTES),
                new RequestedResource("ssd", Units.Bytes.GIBIBYTES),
                new RequestedResource("io_net", Units.BinaryBytesPerSecond.MEBIBYTES_PER_SECOND));
        this.ypSegments = List.of(Set.of("KLG", "SAS", "VLA"), Set.of("default", "dev"));
        this.saasSegments = Set.of("KLG", "SAS", "VLA");
        this.reducedOrderSubsetSegmentKeys = Set.of("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 (RequestedResource resource : ypResources) {
            for (String ypLocation : ypSegments.get(0)) {
                for (String ypSegment : ypSegments.get(1)) {
                    List<Long> selectedOrders = reducedOrderSubsetSegmentKeys.contains(ypLocation)
                            ? ordersReducedSubset : orders;
                    for (Long orderId : selectedOrders) {
                        long value = currentValue++;
                        builder.addChange(CreateChange.builder()
                                .serviceKey(YP_PROVIDER_KEY)
                                .orderId(orderId)
                                .resourceKey(resource.getKey())
                                .addSegmentKey(ypLocation)
                                .addSegmentKey(ypSegment)
                                .amount(Amount.builder()
                                        .value(value)
                                        .unit(resource.getUnit())
                                        .build())
                                .build());
                    }
                }
            }
        }
        for (RequestedResource resource : saasResources) {
            for (String saasLocation : saasSegments) {
                List<Long> selectedOrders = reducedOrderSubsetSegmentKeys.contains(saasLocation)
                        ? ordersReducedSubset : orders;
                for (Long orderId : selectedOrders) {
                    long value = currentValue++;
                    builder.addChange(CreateChange.builder()
                            .serviceKey(SAAS_PROVIDER_KEY)
                            .orderId(orderId)
                            .resourceKey(resource.getKey())
                            .addSegmentKey(saasLocation)
                            .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<>();
        request.getBaseResourceChanges().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 ypLocation : ypSegments.get(0)) {
                for (String ypSegment : ypSegments.get(1)) {
                    RequestedAmount requestedYpCpu = resources.get(new SegmentedResourceKey(YP_PROVIDER_KEY,
                            "cpu_segmented", Set.of(ypLocation, ypSegment)));
                    RequestedAmount requestedYpRam = resources.get(new SegmentedResourceKey(YP_PROVIDER_KEY,
                            "ram_segmented", Set.of(ypLocation, ypSegment)));
                    RequestedAmount requestedYpHdd = resources.get(new SegmentedResourceKey(YP_PROVIDER_KEY,
                            "hdd_segmented", Set.of(ypLocation, ypSegment)));
                    RequestedAmount requestedYpSsd = resources.get(new SegmentedResourceKey(YP_PROVIDER_KEY,
                            "ssd_segmented", Set.of(ypLocation, ypSegment)));
                    RequestedAmount requestedYpHddIo = resources.get(new SegmentedResourceKey(YP_PROVIDER_KEY,
                            "io_hdd", Set.of(ypLocation, ypSegment)));
                    RequestedAmount requestedYpSsdIo = resources.get(new SegmentedResourceKey(YP_PROVIDER_KEY,
                            "io_ssd", Set.of(ypLocation, ypSegment)));
                    RequestedAmount requestedYpNetIo = resources.get(new SegmentedResourceKey(YP_PROVIDER_KEY,
                            "io_net", Set.of(ypLocation, ypSegment)));
                    if (requestedYpCpu != null) {
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:cpu",
                                        Set.of(ypLocation, ypSegment)), requestedYpCpu);
                    }
                    if (requestedYpRam != null) {
                        if (!requestedYpRam.getUnit().equals(Units.Bytes.BYTES)) {
                            LOG.error("Unexpected YP RAM unit: {}", requestedYpRam);
                            return Result.failure(ErrorCollection.builder().addError("Unexpected YP RAM unit").build());
                        }
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:ram",
                                        Set.of(ypLocation, ypSegment)), new RequestedAmount(toGibibytes(requestedYpRam
                                        .getAmount()), Units.Gibibytes.GIBIBYTES));
                    }
                    if (requestedYpHdd != null) {
                        if (!requestedYpHdd.getUnit().equals(Units.Bytes.BYTES)) {
                            LOG.error("Unexpected YP HDD unit: {}", requestedYpHdd);
                            return Result.failure(ErrorCollection.builder().addError("Unexpected YP HDD unit").build());
                        }
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:hdd",
                                        Set.of(ypLocation, ypSegment)), new RequestedAmount(toGibibytes(requestedYpHdd
                                        .getAmount()), Units.Gibibytes.GIBIBYTES));
                    }
                    if (requestedYpSsd != null) {
                        if (!requestedYpSsd.getUnit().equals(Units.Bytes.BYTES)) {
                            LOG.error("Unexpected YP SSD unit: {}", requestedYpSsd);
                            return Result.failure(ErrorCollection.builder().addError("Unexpected YP SSD unit").build());
                        }
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:ssd",
                                        Set.of(ypLocation, ypSegment)), new RequestedAmount(toGibibytes(requestedYpSsd
                                        .getAmount()), Units.Gibibytes.GIBIBYTES));
                    }
                    if (requestedYpHddIo != null) {
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_hdd",
                                        Set.of(ypLocation, ypSegment)), requestedYpHddIo);
                    }
                    if (requestedYpSsdIo != null) {
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_ssd",
                                        Set.of(ypLocation, ypSegment)), requestedYpSsdIo);
                    }
                    if (requestedYpNetIo != null) {
                        expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                                .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_net",
                                        Set.of(ypLocation, ypSegment)), requestedYpNetIo);
                    }
                }
            }
            for (String saasLocation : saasSegments) {
                RequestedAmount requestedSaasCpu = resources.get(new SegmentedResourceKey(SAAS_PROVIDER_KEY,
                        "cpu", Set.of(saasLocation)));
                RequestedAmount requestedSaasRam = resources.get(new SegmentedResourceKey(SAAS_PROVIDER_KEY,
                        "ram", Set.of(saasLocation)));
                RequestedAmount requestedSaasSsd = resources.get(new SegmentedResourceKey(SAAS_PROVIDER_KEY,
                        "ssd", Set.of(saasLocation)));
                RequestedAmount requestedSaasNetIo = resources.get(new SegmentedResourceKey(SAAS_PROVIDER_KEY,
                        "io_net", Set.of(saasLocation)));
                RequestedAmount currentCpu = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                        new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:cpu",
                        Set.of(saasLocation, "default")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                if (requestedSaasCpu != null) {
                    if (!currentCpu.getUnit().equals(requestedSaasCpu.getUnit())) {
                        LOG.error("Unexpected YP CPU unit: {} vs {}", currentCpu, requestedSaasCpu);
                        return Result.failure(ErrorCollection.builder().addError("Unexpected YP CPU unit").build());
                    }
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:cpu",
                                    Set.of(saasLocation, "default")), new RequestedAmount(currentCpu.getAmount()
                                    + requestedSaasCpu.getAmount(), requestedSaasCpu.getUnit()));
                }
                RequestedAmount currentRam = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                        new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:ram",
                                Set.of(saasLocation, "default")), new RequestedAmount(0,
                                Units.Gibibytes.GIBIBYTES));
                if (requestedSaasRam != null) {
                    if (!requestedSaasRam.getUnit().equals(Units.Bytes.BYTES)) {
                        LOG.error("Unexpected Saas RAM unit: {}", requestedSaasRam);
                        return Result.failure(ErrorCollection.builder().addError("Unexpected Saas RAM unit").build());
                    }
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:ram",
                                    Set.of(saasLocation, "default")), new RequestedAmount(currentRam.getAmount()
                                    + toGibibytes(requestedSaasRam.getAmount()), Units.Gibibytes.GIBIBYTES));
                }
                RequestedAmount currentSsd = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                        new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:ssd",
                                Set.of(saasLocation, "default")), new RequestedAmount(0,
                                Units.Gibibytes.GIBIBYTES));
                if (requestedSaasSsd != null) {
                    if (!requestedSaasSsd.getUnit().equals(Units.Bytes.BYTES)) {
                        LOG.error("Unexpected Saas SSD unit: {}", requestedSaasSsd);
                        return Result.failure(ErrorCollection.builder().addError("Unexpected Saas SSD unit").build());
                    }
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:ssd",
                                    Set.of(saasLocation, "default")), new RequestedAmount(currentSsd.getAmount()
                                    + toGibibytes(requestedSaasSsd.getAmount()), Units.Gibibytes.GIBIBYTES));
                }
                RequestedAmount currentNetIo = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                        new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_net",
                                Set.of(saasLocation, "default")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                if (requestedSaasNetIo != null) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_net",
                                    Set.of(saasLocation, "default")), new RequestedAmount(currentNetIo.getAmount()
                                    + requestedSaasNetIo.getAmount(), requestedSaasNetIo.getUnit()));
                }
                RequestedAmount currentHdd = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                        new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:hdd",
                                Set.of(saasLocation, "default")), new RequestedAmount(0,
                                Units.Gibibytes.GIBIBYTES));
                if (requestedSaasCpu != null && requestedSaasRam != null && requestedSaasSsd != null) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:hdd",
                                    Set.of(saasLocation, "default")), new RequestedAmount(currentHdd.getAmount()
                                    + Math.round(Math.ceil(((double) (
                                            28L * 2L * 1024L * 1024L * 1024L * requestedSaasCpu.getAmount()
                                            + 7L * 2L * 1000L * requestedSaasRam.getAmount()
                                            + 1000L * requestedSaasSsd.getAmount()))
                                    / ((double) (1000L * 2L * 1024L * 1024L * 1024L)))), Units.Gibibytes.GIBIBYTES));
                }
                RequestedAmount currentHddIo = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                        new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_hdd",
                                Set.of(saasLocation, "default")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                if (requestedSaasCpu != null && requestedSaasRam != null && requestedSaasSsd != null) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_hdd",
                                    Set.of(saasLocation, "default")), new RequestedAmount(currentHddIo.getAmount()
                                    + Math.round(Math.ceil(((double) (
                                            512L * 512L * 4L * 1024L * requestedSaasCpu.getAmount()
                                            + 125L * requestedSaasRam.getAmount()
                                            + 25L * requestedSaasSsd.getAmount()))
                                            / ((double) (25L * 100L * 1024L)))),
                                    Units.BinaryBytesPerSecond.BINARY_BYTES_PER_SECOND));
                }
                RequestedAmount currentSsdIo = expectedBaseResources.getOrDefault(orderId, Map.of()).getOrDefault(
                        new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_ssd",
                                Set.of(saasLocation, "default")), new RequestedAmount(0, Units.Cores.PERMILLE_CORES));
                if (requestedSaasCpu != null && requestedSaasRam != null && requestedSaasSsd != null) {
                    expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                            .put(new SegmentedResourceKey(YP_PROVIDER_KEY, "yp:io_ssd",
                                    Set.of(saasLocation, "default")), new RequestedAmount(currentSsdIo.getAmount()
                                    + Math.round(Math.ceil(((double) (
                                            512L * 512L * 512L * requestedSaasCpu.getAmount()
                                            + 30L * requestedSaasRam.getAmount()
                                            + 5L * requestedSaasSsd.getAmount()))
                                    / ((double) (40L * 512L)))),
                                    Units.BinaryBytesPerSecond.BINARY_BYTES_PER_SECOND));
                }
            }
        }
        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 {
            return Result.failure(ErrorCollection.builder().addError("Base resource mismatch for " + providerName())
                    .build());
        }
    }

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

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

}
