<?php

declare(strict_types=1);

namespace apexl\Io\modules\system\hooks;

use apexl\Config\ConfigurationInterface;
use apexl\Config\Exceptions\MissingConfigException;
use apexl\Io\interfaces\AbstractHook;
use apexl\Io\middleware\AddCorsHeaders;
use apexl\Io\middleware\AddRouteToHttpPaths;
use apexl\Io\middleware\HandleCorsPreflight;
use apexl\Io\modules\system\collections\MiddlewareCollectionCollection;
use apexl\Io\modules\system\exceptions\WhoopsSetupException;
use apexl\Io\modules\system\services\MiddlewareCollector;
use apexl\Io\modules\system\services\WhoopsHandlerCollector;
use app\vendor\apexl\io\src\Io\middleware\AddBuildHeader;
use app\vendor\apexl\io\src\Io\middleware\InjectIoRequest;
use app\vendor\apexl\io\src\Io\middleware\RequireSsl;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
use Slim\Exception\HttpException;
use Slim\Handlers\ErrorHandler;
use Slim\Middleware\BodyParsingMiddleware;
use Slim\Middleware\ErrorMiddleware;
use Slim\Middleware\MethodOverrideMiddleware;
use Slim\Middleware\RoutingMiddleware;
use Throwable;
use Zeuxisoo\Whoops\Slim\WhoopsMiddleware;

final readonly class AddMiddleware implements AbstractHook
{
    public function __construct(
        private App $application,
        private WhoopsHandlerCollector $whoopsHandlerCollector,
        private ConfigurationInterface $config,
    ) {}

    public function hooks(): array
    {
        return [MiddlewareCollector::HOOK__COLLECT];
    }

    /**
     * @throws MissingConfigException
     * @throws WhoopsSetupException
     */
    public function __invoke(MiddlewareCollectionCollection $weightedMiddleware): MiddlewareCollectionCollection
    {
        $routingMiddleware = new RoutingMiddleware(
            $this->application->getRouteResolver(),
            $this->application->getRouteCollector()->getRouteParser()
        );

        // Whoops first so its set_exception_handler is installed before any other
        // middleware (RequireSsl, BodyParsing, Routing, …) can throw.
        $this->maybeAddWhoopsMiddleware($weightedMiddleware);
        $weightedMiddleware->add(AddCorsHeaders::class, weight: MiddlewareCollector::WEIGHT__FIRST);
        // Inside CORS (so preflight responses get CORS headers) and before routing
        // (so a preflight never dispatches a controller / permission middleware).
        $weightedMiddleware->add(HandleCorsPreflight::class, weight: MiddlewareCollector::WEIGHT__FIRST);
        // Around routing so HttpNotFound/HttpMethodNotAllowed render natively (404/405);
        // non-HTTP throwables are re-thrown to the outer AddCorsHeaders/Whoops handling.
        $weightedMiddleware->add($this->errorMiddleware(), weight: MiddlewareCollector::WEIGHT__FIRST);
        if ($this->config->get('system.require_ssl')) {
            $weightedMiddleware->add(RequireSsl::class, weight: MiddlewareCollector::WEIGHT__FIRST);
        }
        $weightedMiddleware->add(BodyParsingMiddleware::class, weight: MiddlewareCollector::WEIGHT__FIRST);
        $weightedMiddleware->add(MethodOverrideMiddleware::class, weight: MiddlewareCollector::WEIGHT__FIRST);
        $weightedMiddleware->add($routingMiddleware, weight: MiddlewareCollector::WEIGHT__FIRST);
        $weightedMiddleware->add(InjectIoRequest::class, weight: MiddlewareCollector::WEIGHT__FIRST);
        $weightedMiddleware->add(AddRouteToHttpPaths::class, weight: MiddlewareCollector::WEIGHT__FIRST);

        $weightedMiddleware->add(AddBuildHeader::class, weight: MiddlewareCollector::WEIGHT__LAST);


        return $weightedMiddleware;
    }

    /**
     * @throws MissingConfigException
     * @throws WhoopsSetupException
     */
    /**
     * Slim's ErrorMiddleware, but scoped to HTTP exceptions only: 404/405 (and other
     * HttpExceptions) render via Slim's default handler, while anything else is
     * re-thrown so the existing AddCorsHeaders/Whoops error handling stays in charge.
     *
     * @throws MissingConfigException
     */
    private function errorMiddleware(): ErrorMiddleware
    {
        $displayErrorDetails = in_array($this->config->get('app.environment'), ['local', 'development'], true);

        $errorMiddleware = new ErrorMiddleware(
            $this->application->getCallableResolver(),
            $this->application->getResponseFactory(),
            displayErrorDetails: $displayErrorDetails,
            logErrors: false,
            logErrorDetails: false,
        );

        $renderHttpException = new ErrorHandler(
            $this->application->getCallableResolver(),
            $this->application->getResponseFactory(),
        );

        $errorMiddleware->setDefaultErrorHandler(
            function (
                ServerRequestInterface $request,
                Throwable $exception,
                bool $displayErrorDetails,
                bool $logErrors,
                bool $logErrorDetails,
            ) use ($renderHttpException): ResponseInterface {
                if (!$exception instanceof HttpException) {
                    throw $exception;
                }

                return $renderHttpException($request, $exception, $displayErrorDetails, $logErrors, $logErrorDetails);
            }
        );

        return $errorMiddleware;
    }

    private function maybeAddWhoopsMiddleware(MiddlewareCollectionCollection $weightedMiddleware): void
    {
        if ($this->config->get('whoops.enable')) {
            $handlers = $this->whoopsHandlerCollector->collect();

            $weightedMiddleware->add(new WhoopsMiddleware(handlers: $handlers), weight: MiddlewareCollector::WEIGHT__FIRST);
        }
    }
}
