<?php

namespace apexl\Io\includes;

use apexl\Config\Exceptions\MissingConfigException;
use apexl\Io\enums\HttpMethod;
use apexl\Io\exceptions\RouteNotFoundException;
use apexl\Io\factories\RouteFactory;
use apexl\Io\services\HttpPaths;
use apexl\Io\utilityIncludes\Route;
use Psr\Http\Server\MiddlewareInterface;
use Slim\App;
use function apexl\Io\assertInstanceOf;

final class RouteManager
{
    public function __construct(
        private readonly App $application,
        private readonly HttpPaths $httpPaths,
        private readonly HookManager $hookManager,
        private readonly RouteFactory $routeFactory,
        protected string $baseRoute = '/',
        /** @var Route[] */
        public array $routes = [] {
            get {
                return $this->hookManager->processHook('routePreBuild', $this->routes);
            }
        },
    ) {}

    /**
     * @param (MiddlewareInterface|class-string<MiddlewareInterface>)[] $middleware
     * @param callable|class-string<Controller> $callable
     */
    public function post(string $name, string $pattern, callable|string $callable, array $middleware = []): void
    {
        $this->addRoute(HttpMethod::POST, $name, $pattern, $callable, $middleware);
    }

    /**
     * @param (MiddlewareInterface|class-string<MiddlewareInterface>)[] $middleware
     * @param callable|class-string<Controller> $callable
     */
    public function addRoute(HttpMethod $method, string $name, string $pattern, callable|string $callable, array $middleware = [], $args = []): void
    {
        $routes = $this->routes;
        $routes[$name] = $this->routeFactory->make(
            $name,
            $pattern,
            $callable,
            $method,
            $args,
            $middleware,
        );
        $this->routes = $routes;
    }

    /**
     * @param (MiddlewareInterface|class-string<MiddlewareInterface>)[] $middleware
     * @param callable|class-string<Controller> $callable
     */
    public function put(string $name, string $pattern, callable|string $callable, array $middleware = []): void
    {
        $this->addRoute(HttpMethod::PUT, $name, $pattern, $callable, $middleware);
    }

    /**
     * @param (MiddlewareInterface|class-string<MiddlewareInterface>)[] $middleware
     * @param callable|class-string<Controller> $callable
     */
    public function delete(string $name, string $pattern, callable|string $callable, array $middleware = []): void
    {
        $this->addRoute(HttpMethod::DELETE, $name, $pattern, $callable, $middleware);
    }

    /**
     * @param (MiddlewareInterface|class-string<MiddlewareInterface>)[] $middleware
     * @param callable|class-string<Controller> $callable
     */
    public function patch(string $name, string $pattern, callable|string $callable, array $middleware = []): void
    {
        $this->addRoute(HttpMethod::PATCH, $name, $pattern, $callable, $middleware);
    }

    /**
     * Set a route object
     */
    public function setRoute(string $name, Route $route): void
    {
        $routes = $this->routes;
        $routes[$name] = $route;

        $this->routes = $routes;
    }

    /**
     * Function to build the set routes into a format Slim can understand.
     */
    public function buildRoutes(): void
    {
        foreach ($this->routes as $route) {
            /** @var \Slim\Routing\Route $slimRoute */
            $slimRoute = $this->application->{$route->getMethod()->forSlim()}(
                sprintf(
                    '%s/%s',
                    rtrim($this->baseRoute, '/'),
                    ltrim($route->getPattern(stripTags: false), '/'),
                ),
                $route->getCallable()
            );

            $slimRoute->setName($route->getName());

            foreach ($route->getArgs() as $argName => $argVal) {
                $slimRoute->setArgument($argName, $argVal);
            }

            foreach ($route->getMiddleware() as $middleware) {
                if (is_string($middleware)) {
                    $middleware = $this->application->getContainer()->get($middleware);
                }

                assertInstanceOf($middleware, MiddlewareInterface::class);
                $slimRoute->addMiddleware($middleware);
            }
        }
    }

    /**
     * @param (MiddlewareInterface|class-string<MiddlewareInterface>)[] $middleware
     * @param callable|class-string<Controller> $callable
     */
    public function get(string $name, string $pattern, callable|string $callable, array $middleware = []): void
    {
        $this->addRoute(HttpMethod::GET, $name, $pattern, $callable, $middleware);
    }

    /**
     * Add arg to already built route, store reference in routes static
     * @throws RouteNotFoundException
     */
    public function addRouteArg(string $name, string $arg, mixed $value): void
    {
        $this->httpPaths->setCurrentRouteArgs($arg, $value);

        if ($route = $this->getRoute($name)) {
            $route->setArg($arg, $value);
        }
    }

    /**
     * Get a route object
     * @throws RouteNotFoundException
     */
    public function getRoute($name): Route
    {
        if (!isset($this->routes[$name])) {
            throw new RouteNotFoundException(sprintf('No route found for \'%s\'', $name));
        }
        return $this->routes[$name];
    }

    /**
     * @throws RouteNotFoundException
     * @throws MissingConfigException
     */
    public function getFullyQualifiedRoutePattern(
        string $name,
        array $replacements = [],
        bool $stripTags = true
    ): string|false {
        return $this->getRoute($name)?->getFullyQualifiedPattern($replacements, $stripTags);
    }

    /**
     * Get a route object
     * @throws RouteNotFoundException
     */
    public function getRoutePattern(
        string $name,
        array $replacements = [],
        bool $stripTags = true
    ): ?string {
        return $this->getRoute($name)?->getPattern($replacements, $stripTags);
    }
}
