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

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;

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

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

    private static final String PROVIDER_KEY = "ydb";

    private final CommonSettings commonSettings;
    private final List<RequestedResource> resources;
    private final Set<String> segmentKeys;

    public YdbValidator(CommonSettings commonSettings) {
        this.commonSettings = commonSettings;
        this.resources = List.of(new RequestedResource("total_ram", Units.Bytes.GIBIBYTES),
                new RequestedResource("total_cpu", Units.Cores.CORES),
                new RequestedResource("storage_groups", Units.Count.COUNT));
        this.segmentKeys = Set.of("ydb-ru", "ydb-ru-prestable", "ydb-eu");
    }

    @Override
    public CreateQuotaRequest prepareTestRequest(Environment environment, CampaignType campaignType) {
        CreateQuotaRequest.Builder builder = commonSettings.baseQuotaRequest(providerName());
        List<Long> orders = commonSettings.orders(campaignType, OrdersSubset.FULL);
        long currentValue = 1L;
        for (RequestedResource resource : resources) {
            for (String segmentKey : segmentKeys) {
                for (Long orderId : orders) {
                    long value = currentValue++;
                    builder.addChange(CreateChange.builder()
                            .serviceKey(PROVIDER_KEY)
                            .orderId(orderId)
                            .resourceKey(resource.getKey())
                            .addSegmentKey(segmentKey)
                            .amount(Amount.builder()
                                    .value(value)
                                    .unit(resource.getUnit())
                                    .build())
                            .build());
                }
            }
        }
        return builder.build();
    }

    @Override
    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> resourcesMap = entry.getValue();
            RequestedAmount requestedTotalRamRu = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "total_ram", Set.of("ydb-ru")));
            RequestedAmount requestedTotalRamRuPrestable = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "total_ram", Set.of("ydb-ru-prestable")));
            RequestedAmount requestedTotalRamEu = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "total_ram", Set.of("ydb-eu")));
            RequestedAmount requestedTotalCpuRu = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "total_cpu", Set.of("ydb-ru")));
            RequestedAmount requestedTotalCpuRuPrestable = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "total_cpu", Set.of("ydb-ru-prestable")));
            RequestedAmount requestedTotalCpuEu = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "total_cpu", Set.of("ydb-eu")));
            RequestedAmount requestedStorageGroupsRu = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "storage_groups", Set.of("ydb-ru")));
            RequestedAmount requestedStorageGroupsRuPrestable = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "storage_groups", Set.of("ydb-ru-prestable")));
            RequestedAmount requestedStorageGroupsEu = resourcesMap.get(new SegmentedResourceKey(PROVIDER_KEY,
                    "storage_groups", Set.of("ydb-eu")));
            if (!requestedTotalRamRu.getUnit().equals(Units.Bytes.BYTES)) {
                LOG.error("Unexpected total RAM ru unit: {}", requestedTotalRamRu);
                return Result.failure(ErrorCollection.builder().addError("Unexpected total RAM ru unit").build());
            }
            if (!requestedTotalRamRuPrestable.getUnit().equals(Units.Bytes.BYTES)) {
                LOG.error("Unexpected total RAM ru prestable unit: {}", requestedTotalRamRuPrestable);
                return Result.failure(ErrorCollection.builder().addError("Unexpected total RAM ru prestable unit")
                        .build());
            }
            if (!requestedTotalRamEu.getUnit().equals(Units.Bytes.BYTES)) {
                LOG.error("Unexpected total RAM eu unit: {}", requestedTotalRamEu);
                return Result.failure(ErrorCollection.builder().addError("Unexpected total RAM eu unit").build());
            }
            if (!requestedTotalCpuRu.getUnit().equals(Units.Cores.PERMILLE_CORES)) {
                LOG.error("Unexpected total CPU ru unit: {}", requestedTotalCpuRu);
                return Result.failure(ErrorCollection.builder().addError("Unexpected total CPU ru unit")
                        .build());
            }
            if (!requestedTotalCpuRuPrestable.getUnit().equals(Units.Cores.PERMILLE_CORES)) {
                LOG.error("Unexpected total CPU ru prestable unit: {}", requestedTotalCpuRuPrestable);
                return Result.failure(ErrorCollection.builder()
                        .addError("Unexpected total CPU ru prestable unit").build());
            }
            if (!requestedTotalCpuEu.getUnit().equals(Units.Cores.PERMILLE_CORES)) {
                LOG.error("Unexpected total CPU eu unit: {}", requestedTotalCpuEu);
                return Result.failure(ErrorCollection.builder().addError("Unexpected total CPU eu unit")
                        .build());
            }
            if (!requestedStorageGroupsRu.getUnit().equals(Units.Count.PERMILLES)) {
                LOG.error("Unexpected storage groups ru unit: {}", requestedTotalCpuRu);
                return Result.failure(ErrorCollection.builder().addError("Unexpected storage groups ru unit")
                        .build());
            }
            if (!requestedStorageGroupsRuPrestable.getUnit().equals(Units.Count.PERMILLES)) {
                LOG.error("Unexpected storage groups ru prestable unit: {}", requestedTotalCpuRuPrestable);
                return Result.failure(ErrorCollection.builder()
                        .addError("Unexpected storage groups ru prestable unit").build());
            }
            if (!requestedStorageGroupsEu.getUnit().equals(Units.Count.PERMILLES)) {
                LOG.error("Unexpected storage groups eu unit: {}", requestedTotalCpuEu);
                return Result.failure(ErrorCollection.builder().addError("Unexpected storage groups eu unit")
                        .build());
            }
            expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                    .put(new SegmentedResourceKey(PROVIDER_KEY, "ydb:total_ram",
                            Set.of("ydb-ru-base")), new RequestedAmount(toGibibytes(requestedTotalRamRu.getAmount() +
                            requestedTotalRamRuPrestable.getAmount()), Units.Gibibytes.GIBIBYTES));
            expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                    .put(new SegmentedResourceKey(PROVIDER_KEY, "ydb:total_ram",
                            Set.of("ydb-eu-base")), new RequestedAmount(toGibibytes(requestedTotalRamEu.getAmount()),
                            Units.Gibibytes.GIBIBYTES));
            expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                    .put(new SegmentedResourceKey(PROVIDER_KEY, "ydb:total_cpu",
                            Set.of("ydb-ru-base")), new RequestedAmount(requestedTotalCpuRu.getAmount() +
                            requestedTotalCpuRuPrestable.getAmount(), Units.Cores.PERMILLE_CORES));
            expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                    .put(new SegmentedResourceKey(PROVIDER_KEY, "ydb:total_cpu",
                            Set.of("ydb-eu-base")), new RequestedAmount(requestedTotalCpuEu.getAmount(),
                            Units.Cores.PERMILLE_CORES));
            expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                    .put(new SegmentedResourceKey(PROVIDER_KEY, "ydb:storage_groups",
                            Set.of("ydb-ru-base")), new RequestedAmount(requestedStorageGroupsRu.getAmount() +
                            requestedStorageGroupsRuPrestable.getAmount(), Units.Count.PERMILLES));
            expectedBaseResources.computeIfAbsent(orderId, k -> new HashMap<>())
                    .put(new SegmentedResourceKey(PROVIDER_KEY, "ydb:storage_groups",
                            Set.of("ydb-eu-base")), new RequestedAmount(requestedStorageGroupsEu.getAmount(),
                            Units.Count.PERMILLES));
        }
        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 "YDB";
    }

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

}
