<?php
declare(strict_types=1);

final class Router
{
    private array $routes = [];
    private array $globalMiddleware = [];
    private $notFoundHandler = null;
    private $methodNotAllowedHandler = null;

    public function use(callable ...$middleware): self
    {
        array_push($this->globalMiddleware, ...$middleware);
        return $this;
    }

    public function add(
        string|array $methods,
        string $pattern,
        callable|array $handler,
        array $middleware = []
    ): self {
        $methods = array_map('strtoupper', (array)$methods);

        $regex = preg_replace_callback('#\{(\w+)(?::([^}]+))?\}#', function ($m) {
            $name = $m[1];
            $pattern = $m[2] ?? '[^/]+';
            return '(?P<' . $name . '>' . $pattern . ')';
        }, rtrim($pattern, '/'));

        $regex = '#^' . ($regex === '' ? '/' : $regex) . '$#u';

        $this->routes[] = [
            'methods'    => $methods,
            'pattern'    => $pattern,
            'regex'      => $regex,
            'handler'    => $handler,
            'middleware' => $middleware,
        ];

        return $this;
    }

    public function get(string $pattern, callable|array $handler, array $mw = []): self { return $this->add('GET', $pattern, $handler, $mw); }
    public function post(string $pattern, callable|array $handler, array $mw = []): self { return $this->add('POST', $pattern, $handler, $mw); }
    public function put(string $pattern, callable|array $handler, array $mw = []): self { return $this->add('PUT', $pattern, $handler, $mw); }
    public function patch(string $pattern, callable|array $handler, array $mw = []): self { return $this->add('PATCH', $pattern, $handler, $mw); }
    public function delete(string $pattern, callable|array $handler, array $mw = []): self { return $this->add('DELETE', $pattern, $handler, $mw); }

    public function group(string $prefix, callable $defineRoutes, array $middleware = []): self
    {
        $prefix = '/' . trim($prefix, '/');
        $outer = $this;
        $groupRouter = new class($outer, $prefix, $middleware) {
            public function __construct(
                private Router $root,
                private string $prefix,
                private array $middleware
            ) {}
            private function add(string|array $methods, string $pattern, $handler, array $mw): void {
                $this->root->add(
                    $methods,
                    rtrim($this->prefix . '/' . ltrim($pattern, '/'), '/'),
                    $handler,
                    array_merge($this->middleware, $mw)
                );
            }
            public function get($p, $h, $mw=[]){ $this->add('GET', $p, $h, $mw); }
            public function post($p, $h, $mw=[]){ $this->add('POST', $p, $h, $mw); }
            public function put($p, $h, $mw=[]){ $this->add('PUT', $p, $h, $mw); }
            public function patch($p, $h, $mw=[]){ $this->add('PATCH', $p, $h, $mw); }
            public function delete($p, $h, $mw=[]){ $this->add('DELETE', $p, $h, $mw); }
        };

        $defineRoutes($groupRouter);
        return $this;
    }

    public function notFound(callable $handler): self { $this->notFoundHandler = $handler; return $this; }
    public function methodNotAllowed(callable $handler): self { $this->methodNotAllowedHandler = $handler; return $this; }

    public function dispatch(?string $method = null, ?string $uri = null): void
    {
        $method = strtoupper($method ?? $_SERVER['REQUEST_METHOD'] ?? 'GET');
        $uri    = $uri ?? parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
        $uri    = '/' . trim($uri, '/');

        $allowedForPath = [];

        foreach ($this->routes as $route) {
            if (!preg_match($route['regex'], rtrim($uri, '/'), $matches)) continue;

            $allowedForPath = array_merge($allowedForPath, $route['methods']);

            if (!in_array($method, $route['methods'], true)) continue;

            $params = [];
            foreach ($matches as $k => $v) if (!is_int($k)) $params[$k] = $v;

            $pipeline = array_merge($this->globalMiddleware, $route['middleware'], [
                function ($req, $res, $next) use ($route, $params) {
                    return $this->invokeHandler($route['handler'], $params, $req, $res);
                }
            ]);

            $this->runPipeline($pipeline, $this->makeRequest($method, $uri, $params), $this->makeResponse());
            return;
        }

        if (!empty($allowedForPath)) {
            http_response_code(405);
            header('Allow: ' . implode(', ', array_unique($allowedForPath)));
            $handler = $this->methodNotAllowedHandler ?? fn() => print 'Method Not Allowed';
            $handler();
            return;
        }

        http_response_code(404);
        $handler = $this->notFoundHandler ?? fn() => print 'Not Found';
        $handler();
    }

    private function runPipeline(array $middleware, array $req, array $res): void
    {
        $runner = function ($index) use (&$runner, $middleware, $req, $res) {
            if (!isset($middleware[$index])) return null;
            $mw = $middleware[$index];
            return $mw($req, $res, fn() => $runner($index + 1));
        };
        $runner(0);
    }

    private function invokeHandler(callable|array $handler, array $params, array $req, array $res)
    {
        if (is_array($handler)) {
            [$class, $method] = $handler;
            $obj = is_object($class) ? $class : new $class();
            return $obj->$method($req, $res, $params);
        }
        return $handler($req, $res, $params);
    }

    private function makeRequest(string $method, string $uri, array $params): array
    {
        return [
            'method' => $method,
            'uri'    => $uri,
            'params' => $params,
            'query'  => $_GET,
            'body'   => $_POST ?: (json_decode(file_get_contents('php://input'), true) ?? []),
            'headers'=> function_exists('getallheaders') ? (getallheaders() ?: []) : [],
            'files'  => $_FILES,
        ];
    }

    private function makeResponse(): array
    {
        return [
            'json' => function (array $data, int $status = 200): void {
                http_response_code($status);
                header('Content-Type: application/json; charset=utf-8');
                echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            },
            'text' => function (string $text, int $status = 200): void {
                http_response_code($status);
                header('Content-Type: text/plain; charset=utf-8');
                echo $text;
            },
            'html' => function (string $html, int $status = 200): void {
                http_response_code($status);
                header('Content-Type: text/html; charset=utf-8');
                echo $html;
            },
        ];
    }
}
