<?php

namespace Nuwave\Lighthouse;

use GraphQL\Error\DebugFlag;
use GraphQL\Error\Error;
use GraphQL\Error\SyntaxError;
use GraphQL\Executor\ExecutionResult;
use GraphQL\GraphQL as GraphQLBase;
use GraphQL\Language\Parser;
use GraphQL\Server\Helper as GraphQLHelper;
use GraphQL\Server\OperationParams;
use GraphQL\Server\RequestError;
use GraphQL\Type\Schema;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Events\BuildExtensionsResponse;
use Nuwave\Lighthouse\Events\EndExecution;
use Nuwave\Lighthouse\Events\EndOperationOrOperations;
use Nuwave\Lighthouse\Events\ManipulateResult;
use Nuwave\Lighthouse\Events\StartExecution;
use Nuwave\Lighthouse\Events\StartOperationOrOperations;
use Nuwave\Lighthouse\Execution\BatchLoader\BatchLoaderRegistry;
use Nuwave\Lighthouse\Execution\DataLoader\BatchLoader;
use Nuwave\Lighthouse\Execution\ErrorPool;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules;
use Nuwave\Lighthouse\Support\Utils as LighthouseUtils;

/**
 * The main entrypoint to start and end GraphQL execution.
 */
class GraphQL
{
    /**
     * @var \Nuwave\Lighthouse\Schema\SchemaBuilder
     */
    protected $schemaBuilder;

    /**
     * @var \Illuminate\Pipeline\Pipeline
     */
    protected $pipeline;

    /**
     * @var \Illuminate\Contracts\Events\Dispatcher
     */
    protected $eventDispatcher;

    /**
     * @var \Nuwave\Lighthouse\Execution\ErrorPool
     */
    protected $errorPool;

    /**
     * @var \Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules
     */
    protected $providesValidationRules;

    /**
     * @var \GraphQL\Server\Helper
     */
    protected $graphQLHelper;

    /**
     * @var \Illuminate\Contracts\Config\Repository
     */
    protected $configRepository;

    /**
     * Lazily initialized.
     *
     * @var \Closure(
     *   array<\GraphQL\Error\Error> $errors,
     *   callable(\GraphQL\Error\Error $error): ?array<string, mixed>
     * ): array<string, mixed>
     */
    protected $errorsHandler;

    public function __construct(
        SchemaBuilder $schemaBuilder,
        Pipeline $pipeline,
        EventDispatcher $eventDispatcher,
        ErrorPool $errorPool,
        ProvidesValidationRules $providesValidationRules,
        GraphQLHelper $graphQLHelper,
        ConfigRepository $configRepository
    ) {
        $this->schemaBuilder = $schemaBuilder;
        $this->pipeline = $pipeline;
        $this->eventDispatcher = $eventDispatcher;
        $this->errorPool = $errorPool;
        $this->providesValidationRules = $providesValidationRules;
        $this->graphQLHelper = $graphQLHelper;
        $this->configRepository = $configRepository;
    }

    /**
     * Run one ore more GraphQL operations against the schema.
     *
     * @param  \GraphQL\Server\OperationParams|array<int, \GraphQL\Server\OperationParams>  $operationOrOperations
     * @return array<string, mixed>|array<int, array<string, mixed>>
     */
    public function executeOperationOrOperations($operationOrOperations, GraphQLContext $context): array
    {
        $this->eventDispatcher->dispatch(
            new StartOperationOrOperations($operationOrOperations)
        );

        $resultOrResults = LighthouseUtils::applyEach(
            /**
             * @return array<string, mixed>
             */
            function (OperationParams $operationParams) use ($context): array {
                return $this->executeOperation($operationParams, $context);
            },
            $operationOrOperations
        );

        $this->eventDispatcher->dispatch(
            new EndOperationOrOperations($resultOrResults)
        );

        return $resultOrResults;
    }

    /**
     * Run a single GraphQL operation against the schema and get a result.
     *
     * @return array<string, mixed>
     */
    public function executeOperation(OperationParams $params, GraphQLContext $context): array
    {
        $errors = $this->graphQLHelper->validateOperationParams($params);

        $query = $params->query;
        if (! is_string($query) || $query === '') {
            $errors[] = new RequestError(
                'GraphQL Request parameter "query" is required and must not be empty.'
            );
        }

        if (count($errors) > 0) {
            $errors = array_map(
                static function (RequestError $err): Error {
                    return Error::createLocatedError($err);
                },
                $errors
            );

            return $this->serializable(
                new ExecutionResult(null, $errors)
            );
        }
        /** @var string $query Otherwise we would have bailed with an error */
        $result = $this->executeQuery(
            $query,
            $context,
            $params->variables,
            null,
            $params->operation
        );

        return $this->serializable($result);
    }

    /**
     * Execute a GraphQL query on the Lighthouse schema and return the raw result.
     *
     * To render the @see ExecutionResult, you will probably want to call `->toArray($debug)` on it,
     * with $debug being a combination of flags in @see \GraphQL\Error\DebugFlag
     *
     * @param  string|\GraphQL\Language\AST\DocumentNode  $query
     * @param  array<string, mixed>|null  $variables
     * @param  mixed|null  $rootValue
     */
    public function executeQuery(
        $query,
        GraphQLContext $context,
        ?array $variables = [],
        $rootValue = null,
        ?string $operationName = null
    ): ExecutionResult {
        // TODO make executeQuery require a DocumentNode and move this parsing out of here
        if (is_string($query)) {
            try {
                $query = Parser::parse($query);
            } catch (SyntaxError $syntaxError) {
                return new ExecutionResult(null, [$syntaxError]);
            }
        }

        // Building the executable schema might take a while to do,
        // so we do it before we fire the StartExecution event.
        // This allows tracking the time for batched queries independently.
        $schema = $this->schemaBuilder->schema();

        $this->eventDispatcher->dispatch(
            new StartExecution($query, $variables, $operationName, $context)
        );

        $result = GraphQLBase::executeQuery(
            $schema,
            $query,
            $rootValue,
            $context,
            $variables,
            $operationName,
            null,
            $this->providesValidationRules->validationRules()
        );

        /** @var array<\Nuwave\Lighthouse\Execution\ExtensionsResponse|null> $extensionsResponses */
        $extensionsResponses = (array) $this->eventDispatcher->dispatch(
            new BuildExtensionsResponse
        );

        foreach ($extensionsResponses as $extensionsResponse) {
            if ($extensionsResponse !== null) {
                $result->extensions[$extensionsResponse->key()] = $extensionsResponse->content();
            }
        }

        foreach ($this->errorPool->errors() as $error) {
            $result->errors [] = $error;
        }

        // Allow listeners to manipulate the result after each resolved query
        $this->eventDispatcher->dispatch(
            new ManipulateResult($result)
        );

        $this->eventDispatcher->dispatch(
            new EndExecution($result)
        );

        $this->cleanUpAfterExecution();

        return $result;
    }

    protected function cleanUpAfterExecution(): void
    {
        BatchLoaderRegistry::forgetInstances();
        $this->errorPool->clear();

        // TODO remove in v6
        BatchLoader::forgetInstances();
    }

    /**
     * Convert the result to a serializable array.
     *
     * @return array<string, mixed>
     */
    public function serializable(ExecutionResult $result): array
    {
        $result->setErrorsHandler($this->errorsHandler());

        return $result->toArray($this->debugFlag());
    }

    /**
     * @return \Closure(
     *   array<\GraphQL\Error\Error> $errors,
     *   callable(\GraphQL\Error\Error $error): ?array<string, mixed>
     * ): array<string, mixed>
     */
    protected function errorsHandler(): \Closure
    {
        if (! isset($this->errorsHandler)) {
            $this->errorsHandler = function (array $errors, callable $formatter): array {
                // User defined error handlers, implementing \Nuwave\Lighthouse\Execution\ErrorHandler
                // This allows the user to register multiple handlers and pipe the errors through.
                $handlers = [];
                foreach ($this->configRepository->get('lighthouse.error_handlers', []) as $handlerClass) {
                    $handlers [] = app($handlerClass);
                }

                return (new Collection($errors))
                    ->map(function (Error $error) use ($handlers, $formatter): ?array {
                        return $this->pipeline
                            ->send($error)
                            ->through($handlers)
                            ->then(function (?Error $error) use ($formatter): ?array {
                                if ($error === null) {
                                    return null;
                                }

                                return $formatter($error);
                            });
                    })
                    ->filter()
                    ->all();
            };
        }

        return $this->errorsHandler;
    }

    protected function debugFlag(): int
    {
        // If debugging is set to false globally, do not add GraphQL specific
        // debugging info either. If it is true, then we fetch the debug
        // level from the Lighthouse configuration.
        return $this->configRepository->get('app.debug')
            ? (int) $this->configRepository->get('lighthouse.debug')
            : DebugFlag::NONE;
    }

    /**
     * Ensure an executable GraphQL schema is present.
     *
     * @deprecated
     * @see \Nuwave\Lighthouse\Schema\SchemaBuilder::schema()
     */
    public function prepSchema(): Schema
    {
        return $this->schemaBuilder->schema();
    }
}
