package ru.yandex.direct.web.entity.grid.service;

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

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

import graphql.ExecutionResult;
import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import ru.yandex.direct.core.entity.campaign.service.accesschecker.RequestCampaignAccessibilityCheckerProvider;
import ru.yandex.direct.core.entity.user.model.User;
import ru.yandex.direct.grid.processing.context.container.GridGraphQLContext;
import ru.yandex.direct.grid.processing.processor.GridGraphQLProcessor;
import ru.yandex.direct.grid.processing.service.campaign.CampaignAccessService;
import ru.yandex.direct.grid.processing.service.dataloader.GridContextProvider;
import ru.yandex.direct.tracing.Trace;
import ru.yandex.direct.utils.JsonUtils;
import ru.yandex.direct.web.core.security.DirectWebAuthenticationSource;
import ru.yandex.direct.web.entity.grid.model.GridResponse;

import static ru.yandex.direct.grid.processing.configuration.GridProcessingConfiguration.GRAPH_QL_PROCESSOR;
import static ru.yandex.direct.grid.processing.model.api.GdApiResponse.VALIDATION_RESULT;
import static ru.yandex.direct.libs.graphql.GraphqlHelper.addReqId;

/**
 * Сервис для обработки GraphQL-запросов, собирает контекст выполнения операции и передает запрос в
 * {@link GridGraphQLProcessor}
 */
@Service
@ParametersAreNonnullByDefault
public class GridService {
    private static final Logger logger = LoggerFactory.getLogger(GridService.class);
    private static final String ALLOWED_TRACE_OPERATIONS_REGEX = "^\\w+$";

    private final GridGraphQLProcessor gridGraphQlProcessor;
    private final DirectWebAuthenticationSource authenticationSource;
    private final GridErrorProcessingService responseHandlingService;
    private final RequestCampaignAccessibilityCheckerProvider requestCampaignAccessibilityCheckerProvider;
    private final CampaignAccessService campaignAccessService;
    private final GridContextProvider gridContextProvider;

    @Autowired
    public GridService(@Qualifier(GRAPH_QL_PROCESSOR) GridGraphQLProcessor gridGraphQlProcessor,
                       DirectWebAuthenticationSource authenticationSource,
                       GridErrorProcessingService responseHandlingService,
                       RequestCampaignAccessibilityCheckerProvider requestCampaignAccessibilityCheckerProvider,
                       CampaignAccessService campaignAccessService,
                       GridContextProvider gridContextProvider) {
        this.gridGraphQlProcessor = gridGraphQlProcessor;
        this.authenticationSource = authenticationSource;
        this.responseHandlingService = responseHandlingService;
        this.requestCampaignAccessibilityCheckerProvider = requestCampaignAccessibilityCheckerProvider;
        this.campaignAccessService = campaignAccessService;
        this.gridContextProvider = gridContextProvider;
    }

    public GridResponse executeGraphQL(String operationName, String query, Map<String, Object> arguments) {
        replaceTraceMethod(operationName);
        User operator = authenticationSource.getAuthentication().getOperator();
        User subjectUser = authenticationSource.getAuthentication().getSubjectUser();
        requestCampaignAccessibilityCheckerProvider
                .setCustom(campaignAccessService.getCampaignAccessibilityChecker(subjectUser.getClientId()));

        gridContextProvider.setGridContext(buildContext(operator, subjectUser));
        ExecutionResult executionResult = gridGraphQlProcessor
                .processQuery(operationName, query, arguments, gridContextProvider.getGridContext());

        //Логируем ответы с результатами валидации, если это необходимо
        logResultsWithValidationIssues(executionResult);

        addReqId(executionResult.getData());
        List<Object> errors = responseHandlingService.sanitizeErrors(executionResult.getErrors());

        return new GridResponse()
                .withData(executionResult.getData())
                .withErrors(errors)
                .withExtensions(executionResult.getExtensions());
    }

    /**
     * Подменяем метод в текущем открытом трейсе.
     * <p>
     * Чтобы для запросов и мутаций GraphQL в логах трейсов были указаны их названия
     * вместо общего для всех "grid.api"
     */
    static void replaceTraceMethod(@Nullable String operationName) {
        // если хакеры будут присылать в поле operationName всякую ерунду типа
        // <script> или ../../../etc/shadow, не кладём это в название метода
        if (operationName != null && operationName.matches(ALLOWED_TRACE_OPERATIONS_REGEX)) {
            Trace trace = Trace.current();
            trace.setMethod(trace.getMethod() + '.' + operationName);
            Trace.updateMdc();
        }
    }

    private void logResultsWithValidationIssues(ExecutionResult executionResult) {
        Set<String> resolversToLogValidationResult =
                gridContextProvider.getGridContext().getResolversToLogValidationResult();
        if (!resolversToLogValidationResult.isEmpty() && null != executionResult.getData()) {
            Map<String, Object> dataMap = executionResult.getData();
            resolversToLogValidationResult.forEach(resolverName -> {
                Map result = (Map) dataMap.get(resolverName);
                if (null != result) {
                    Map validationResult = (Map) result.get(VALIDATION_RESULT.name());
                    if (!MapUtils.isEmpty(validationResult)) {
                        logger.warn("Got validation result on {}: {}", resolverName, JsonUtils.toJson(result));
                    }
                }
            });
        }
    }

    private static GridGraphQLContext buildContext(User operator, User subjectUser) {
        return new GridGraphQLContext(operator, subjectUser);
    }

}
