<?php

declare(strict_types=1);

namespace App\Core;

use RuntimeException;

/**
 * Router
 *
 * Deliberately simple: routes are registered explicitly in routes/web.php
 * and routes/api.php (no controller attribute/annotation scanning, no
 * magic). Supports static segments, {parameters}, and a named middleware
 * stack per route. This is the whole routing layer — easy to read start
 * to finish in one sitting, which matters more than cleverness for a
 * codebase other developers will extend for years.
 */
final class Router
{
    /** @var array<string, array<int, array{pattern:string, regex:string, params:string[], handler:mixed, middleware:string[]}>> */
    private array $routes = [
        'GET' => [], 'POST' => [], 'PUT' => [], 'PATCH' => [], 'DELETE' => [],
    ];

    /** @var array<string, callable> */
    private array $middlewareRegistry = [];

    private mixed $notFoundHandler = null;

    public function registerMiddleware(string $name, callable $resolver): void
    {
        $this->middlewareRegistry[$name] = $resolver;
    }

    public function get(string $pattern, mixed $handler, array $middleware = []): void
    {
        $this->add('GET', $pattern, $handler, $middleware);
    }

    public function post(string $pattern, mixed $handler, array $middleware = []): void
    {
        $this->add('POST', $pattern, $handler, $middleware);
    }
    
    /** Registers a preflight (CORS OPTIONS) route — pair with a post()/get() to the same pattern for any browser-callable cross-origin endpoint. */
    public function options(string $pattern, mixed $handler, array $middleware = []): void
    {
        $this->add('OPTIONS', $pattern, $handler, $middleware);
    }


    public function put(string $pattern, mixed $handler, array $middleware = []): void
    {
        $this->add('PUT', $pattern, $handler, $middleware);
    }

    public function patch(string $pattern, mixed $handler, array $middleware = []): void
    {
        $this->add('PATCH', $pattern, $handler, $middleware);
    }

    public function delete(string $pattern, mixed $handler, array $middleware = []): void
    {
        $this->add('DELETE', $pattern, $handler, $middleware);
    }

    public function notFound(callable $handler): void
    {
        $this->notFoundHandler = $handler;
    }

    private function add(string $method, string $pattern, mixed $handler, array $middleware): void
    {
        $params = [];
        $regex = preg_replace_callback('#\{([a-zA-Z_][a-zA-Z0-9_]*)\}#', function ($m) use (&$params) {
            $params[] = $m[1];

            // {path} is a reserved catch-all parameter name (used by
            // routes serving nested file paths, e.g. /uploads-view/{path})
            // and greedily matches multiple segments including slashes.
            // Every other {param} matches exactly one segment.
            return $m[1] === 'path' ? '(.+)' : '([^/]+)';
        }, rtrim($pattern, '/') ?: '/');

        $this->routes[$method][] = [
            'pattern' => $pattern,
            'regex' => '#^' . $regex . '$#',
            'params' => $params,
            'handler' => $handler,
            'middleware' => $middleware,
        ];
    }

    public function dispatch(Request $request): Response
    {
        $method = $request->method();
        $path = $request->path();

        foreach ($this->routes[$method] ?? [] as $route) {
            if (preg_match($route['regex'], $path, $matches)) {
                array_shift($matches);
                $params = array_combine($route['params'], $matches);

                return $this->runPipeline($route['middleware'], $route['handler'], $request, $params);
            }
        }

        if ($this->notFoundHandler !== null) {
            return ($this->notFoundHandler)($request);
        }

        return Response::html('404 Not Found', 404);
    }

    private function runPipeline(array $middlewareNames, mixed $handler, Request $request, array $params): Response
    {
        $core = function (Request $req) use ($handler, $params): Response {
            return $this->callHandler($handler, $req, $params);
        };

        // Build the onion from the inside out so middleware listed first
        // in the route definition runs first at request time.
        $pipeline = array_reduce(
            array_reverse($middlewareNames),
            function (callable $next, string $entry) {
                // Supports "permission:leads.create" — everything after
                // the first ':' is passed to the middleware resolver.
                [$name, $param] = array_pad(explode(':', $entry, 2), 2, null);

                if (!isset($this->middlewareRegistry[$name])) {
                    throw new RuntimeException("Unregistered middleware: {$name}");
                }

                $middleware = ($this->middlewareRegistry[$name])($param);

                return fn (Request $req): Response => $middleware->handle($req, $next);
            },
            $core
        );

        return $pipeline($request);
    }

    private function callHandler(mixed $handler, Request $request, array $params): Response
    {
        if (is_callable($handler) && !is_array($handler)) {
            return $handler($request, $params);
        }

        // Handler given as "App\Controllers\LeadController@index"
        if (is_string($handler) && str_contains($handler, '@')) {
            [$class, $method] = explode('@', $handler, 2);
            $controller = new $class();

            return $controller->$method($request, $params);
        }

        if (is_array($handler) && count($handler) === 2) {
            [$class, $method] = $handler;
            $controller = is_object($class) ? $class : new $class();

            return $controller->$method($request, $params);
        }

        throw new RuntimeException('Invalid route handler.');
    }
}
