package ru.yandex.dispenser.validation.validator;

import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import ru.yandex.dispenser.validation.client.CampaignType;
import ru.yandex.dispenser.validation.client.DispenserClient;
import ru.yandex.dispenser.validation.client.Environment;
import ru.yandex.dispenser.validation.client.ErrorCollection;
import ru.yandex.dispenser.validation.client.Response;
import ru.yandex.dispenser.validation.client.Result;
import ru.yandex.dispenser.validation.client.model.CreateQuotaRequest;
import ru.yandex.dispenser.validation.client.model.DispenserError;
import ru.yandex.dispenser.validation.client.model.ExpandQuotaChangeRequest;
import ru.yandex.dispenser.validation.client.model.QuotaRequest;
import ru.yandex.dispenser.validation.client.model.QuotaRequestList;
import ru.yandex.dispenser.validation.client.model.Status;
import ru.yandex.dispenser.validation.providers.CommonSettings;
import ru.yandex.dispenser.validation.providers.ProviderValidator;

/**
 * Validator.
 *
 * @author Dmitriy Timashov <dm-tim@yandex-team.ru>
 */
@Component
public class Validator {

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

    private final List<ProviderValidator> providerValidators;
    private final DispenserClient dispenserClient;
    private final Environment environment;
    private final CommonSettings commonSettings;

    public Validator(List<ProviderValidator> providerValidators,
                     DispenserClient dispenserClient,
                     @Value("${validation.environment}") String environment,
                     CommonSettings commonSettings) {
        this.providerValidators = providerValidators;
        this.dispenserClient = dispenserClient;
        this.environment = Environment.valueOf(environment);
        this.commonSettings = commonSettings;
    }

    public Mono<List<Result<String>>> validate(String token, CampaignType campaignType) {
        Set<ExpandQuotaChangeRequest> expand = Set.of(ExpandQuotaChangeRequest.BASE_RESOURCES);
        return Flux.fromIterable(providerValidators).concatMap(validator -> {
            CreateQuotaRequest testRequest = validator.prepareTestRequest(environment, campaignType);
            return Mono.usingWhen(
                    dispenserClient.createQuotaRequest(testRequest, commonSettings.campaignId(campaignType), expand,
                            environment, token),
                    createResponse -> handleCreatedQuotaRequest(expand, createResponse, validator, token, campaignType),
                    createResponse -> cancelQuotaRequest(expand, createResponse, token)
            ).onErrorResume(ex -> {
                LOG.error("Failed to validate " + validator.providerName(), ex);
                return Mono.just(Result.failure(ErrorCollection.builder()
                        .addError("Failed to validate " + validator.providerName()).build()));
            });
        }).collectList();
    }

    private Mono<Result<String>> handleCreatedQuotaRequest(Set<ExpandQuotaChangeRequest> expand,
                                                           Response<QuotaRequestList, DispenserError> createResponse,
                                                           ProviderValidator validator, String token,
                                                           CampaignType campaignType) {
        String providerName = validator.providerName();
        return createResponse.match(
                (createdRequest, receivedRequestId, sentRequestId) ->
                        validateCreatedQuotaRequest(expand, createdRequest, validator, token, campaignType),
                (ex, sentRequestId) -> {
                    LOG.error("Failed to create quota request, sent request id = " + sentRequestId, ex);
                    return Mono.just(Result.failure(ErrorCollection.builder()
                            .addError("Failed to create quota request for " + providerName).build()));
                },
                (error, receivedRequestId, sentRequestId) -> {
                    LOG.error("Failed to create quota request, sent request id = {}," +
                            " received request id = {}, error = {}", sentRequestId, receivedRequestId,
                            error);
                    return Mono.just(Result.failure(ErrorCollection.builder()
                            .addError("Failed to create quota request for " + providerName).build()));
                }
        );
    }

    private Mono<Result<String>> validateCreatedQuotaRequest(Set<ExpandQuotaChangeRequest> expand,
                                                             QuotaRequestList createdRequest,
                                                             ProviderValidator validator,
                                                             String token,
                                                             CampaignType campaignType) {
        String providerName = validator.providerName();
        long quotaRequestId = createdRequest.getResult().get(0).getId();
        return dispenserClient.getQuotaRequest(quotaRequestId, expand, environment, token)
                .flatMap(getResponse ->
            getResponse.match(
                    (getRequest, receivedGetRequestId, sentGetRequestId) ->
                            Mono.just(validateBaseResources(validator, getRequest, campaignType)),
                    (ex, sentGetRequestId) -> {
                        LOG.error("Failed to get quota request " + quotaRequestId
                                + ", sent request id = " + sentGetRequestId, ex);
                        return Mono.just(Result.failure(ErrorCollection.builder()
                                .addError("Failed to get quota request for " + providerName)
                                .build()));
                    },
                    (error, receivedGetRequestId, sentGetRequestId) -> {
                        LOG.error("Failed to get quota request {}, sent request id = {}," +
                                        " received request id = {}, error = {}", quotaRequestId,
                                sentGetRequestId, receivedGetRequestId, error);
                        return Mono.just(Result.failure(ErrorCollection.builder()
                                .addError("Failed to get quota request for " + providerName)
                                .build()));
                    }
            )
        );
    }

    private Result<String> validateBaseResources(ProviderValidator validator, QuotaRequest quotaRequest,
                                                 CampaignType campaignType) {
        return validator.validateTestRequest(quotaRequest, environment, campaignType);
    }

    private Mono<Object> cancelQuotaRequest(Set<ExpandQuotaChangeRequest> expand,
                                            Response<QuotaRequestList, DispenserError> createResponse,
                                            String token) {
        return createResponse.match(
                (createdRequest, receivedRequestId, sentRequestId) -> {
                    long quotaRequestId = createdRequest.getResult().get(0).getId();
                    return dispenserClient.setQuotaRequestStatus(quotaRequestId, Status.CANCELLED, expand,
                            environment, token).flatMap(updateStatusResult ->
                        updateStatusResult.match(
                                (updatedRequest, receivedUpdateRequestId, sentUpdateRequestId) ->
                                        Mono.empty(),
                                (ex, sentUpdateRequestId) -> {
                                    LOG.error("Failed to cancel quota request " + quotaRequestId
                                            + ", sent request id = " + sentUpdateRequestId, ex);
                                    return Mono.empty();
                                },
                                (error, receivedUpdateRequestId, sentUpdateRequestId) -> {
                                    LOG.error("Failed to cancel quota request {}, sent request id = {},"
                                                    + " received request id = {}, error = {}",
                                            quotaRequestId, sentUpdateRequestId, receivedUpdateRequestId, error);
                                    return Mono.empty();
                                }
                        )
                    ).onErrorResume(ex -> {
                        LOG.error("Failed to cancel quota request " + quotaRequestId, ex);
                        return Mono.empty();
                    });
                },
                (ex, sentRequestId) -> Mono.empty(),
                (error, receivedRequestId, sentRequestId) -> Mono.empty()
        );
    }

}
