<?php
declare(strict_types=1);

namespace Celena\Core\Http;

use Celena\Core\Container;
use Closure;

/**
 * Простой роутер с поддержкой групп, префиксов и параметров пути.
 * Шаблон: /users/{id} или /admin/{section:[a-z\-]+}
 */
final class Router
{
    /** @var list<array{method:string, pattern:string, regex:string, params:string[], handler:mixed, middleware:string[]}> */
    private array $routes = [];

    /** @var array<int, array{prefix:string, middleware:string[]}> */
    private array $groupStack = [];

    /** @var Closure|null */
    private $notFound = null;

    public function __construct(private readonly Container $container)
    {
    }

    public function get(string $path, mixed $handler): self    { return $this->add(['GET', 'HEAD'], $path, $handler); }
    public function post(string $path, mixed $handler): self   { return $this->add(['POST'], $path, $handler); }
    public function put(string $path, mixed $handler): self    { return $this->add(['PUT'], $path, $handler); }
    public function patch(string $path, mixed $handler): self  { return $this->add(['PATCH'], $path, $handler); }
    public function delete(string $path, mixed $handler): self { return $this->add(['DELETE'], $path, $handler); }
    public function any(string $path, mixed $handler): self    { return $this->add(['GET','HEAD','POST','PUT','PATCH','DELETE'], $path, $handler); }

    /** @param array{prefix?:string, middleware?:string[]} $opts */
    public function group(array $opts, Closure $callback): void
    {
        $this->groupStack[] = [
            'prefix' => trim($opts['prefix'] ?? '', '/'),
            'middleware' => $opts['middleware'] ?? [],
        ];
        $callback($this);
        array_pop($this->groupStack);
    }

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

    public function dispatch(Request $request): Response
    {
        $method = $request->method;
        $path = '/' . trim($request->path, '/');
        if ($path === '/') {
            $path = '/';
        }

        foreach ($this->routes as $route) {
            if ($route['method'] !== $method && !($method === 'HEAD' && $route['method'] === 'GET')) {
                continue;
            }
            if (preg_match($route['regex'], $path, $matches)) {
                $params = [];
                foreach ($route['params'] as $name) {
                    $params[$name] = $matches[$name] ?? null;
                }
                return $this->invoke($route['handler'], $request, $params, $route['middleware']);
            }
        }

        if ($this->notFound !== null) {
            $result = ($this->notFound)($request);
            return $result instanceof Response ? $result : Response::notFound();
        }
        return Response::notFound();
    }

    /** @param string[] $methods */
    private function add(array $methods, string $path, mixed $handler): self
    {
        [$prefix, $stackMw] = $this->currentGroup();
        $fullPath = '/' . trim(($prefix === '' ? '' : $prefix . '/') . trim($path, '/'), '/');
        if ($fullPath === '') {
            $fullPath = '/';
        }
        [$regex, $params] = $this->compile($fullPath);
        foreach ($methods as $method) {
            $this->routes[] = [
                'method' => $method,
                'pattern' => $fullPath,
                'regex' => $regex,
                'params' => $params,
                'handler' => $handler,
                'middleware' => $stackMw,
            ];
        }
        return $this;
    }

    /** @return array{0:string, 1:string[]} */
    private function compile(string $path): array
    {
        $params = [];
        $regex = preg_replace_callback(
            '#\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?\}#',
            function (array $m) use (&$params): string {
                $params[] = $m[1];
                $constraint = $m[2] ?? '[^/]+';
                return '(?P<' . $m[1] . '>' . $constraint . ')';
            },
            $path
        );
        return ['#^' . $regex . '$#u', $params];
    }

    /** @return array{0:string, 1:string[]} */
    private function currentGroup(): array
    {
        $prefix = '';
        $mw = [];
        foreach ($this->groupStack as $g) {
            if ($g['prefix'] !== '') {
                $prefix = $prefix === '' ? $g['prefix'] : ($prefix . '/' . $g['prefix']);
            }
            $mw = array_merge($mw, $g['middleware']);
        }
        return [$prefix, $mw];
    }

    /**
     * @param array<string, mixed> $params
     * @param string[] $middleware
     */
    private function invoke(mixed $handler, Request $request, array $params, array $middleware): Response
    {
        $core = function (Request $req) use ($handler, $params): Response {
            $callable = $this->resolve($handler);
            $result = $callable($req, $params);
            return $result instanceof Response ? $result : Response::html((string) $result);
        };

        $next = $core;
        foreach (array_reverse($middleware) as $mwClass) {
            $mw = $this->container->make($mwClass);
            $current = $next;
            $next = fn (Request $r): Response => $mw->handle($r, $current);
        }
        return $next($request);
    }

    private function resolve(mixed $handler): callable
    {
        if (is_callable($handler)) {
            return $handler;
        }
        if (is_string($handler) && str_contains($handler, '@')) {
            [$class, $method] = explode('@', $handler, 2);
            $obj = $this->container->make($class);
            return [$obj, $method];
        }
        if (is_array($handler) && count($handler) === 2 && is_string($handler[0])) {
            $obj = $this->container->make($handler[0]);
            return [$obj, $handler[1]];
        }
        throw new \RuntimeException('Router: cannot resolve handler.');
    }
}
