<?php

declare(strict_types=1);

namespace App\Core;

final class Router
{
    /** @var array<int, array{method:string,pattern:string,handler:callable,name:?string}> */
    private array $routes = [];

    public function add(string $method, string $pattern, callable $handler, ?string $name = null): void
    {
        $this->routes[] = [
            'method' => strtoupper($method),
            'pattern' => $this->normalize($pattern),
            'handler' => $handler,
            'name' => $name,
        ];
    }

    public function get(string $pattern, callable $handler, ?string $name = null): void { $this->add('GET', $pattern, $handler, $name); }
    public function post(string $pattern, callable $handler, ?string $name = null): void { $this->add('POST', $pattern, $handler, $name); }
    public function put(string $pattern, callable $handler, ?string $name = null): void { $this->add('PUT', $pattern, $handler, $name); }
    public function patch(string $pattern, callable $handler, ?string $name = null): void { $this->add('PATCH', $pattern, $handler, $name); }
    public function delete(string $pattern, callable $handler, ?string $name = null): void { $this->add('DELETE', $pattern, $handler, $name); }

    public function dispatch(Request $request): void
    {
        if ($request->method === 'OPTIONS') {
            http_response_code(204);
            return;
        }

        $allowedMethods = [];
        $requestMethod = $request->method === 'HEAD' ? 'GET' : $request->method;

        foreach ($this->routes as $route) {
            $params = $this->match($route['pattern'], $request->path);
            if ($params === null) {
                continue;
            }

            if ($route['method'] !== $requestMethod) {
                $allowedMethods[] = $route['method'];
                continue;
            }

            ($route['handler'])($request, $params);
            return;
        }

        if ($allowedMethods) {
            $allowedMethods = array_values(array_unique($allowedMethods));
            Response::error(
                'Método não permitido para esta rota.',
                405,
                ['allowed_methods' => $allowedMethods],
                ['Allow' => implode(', ', $allowedMethods)]
            );
            return;
        }

        Response::error('Rota não encontrada.', 404, [
            'method' => $request->method,
            'path' => $request->path,
            'hint' => 'Verifique se a URL usa o prefixo /api/v1 e se o servidor aponta para a pasta public.',
        ]);
    }

    /** @return array<int, array{method:string,pattern:string,name:?string}> */
    public function routes(): array
    {
        return array_map(static fn(array $r): array => [
            'method' => $r['method'],
            'pattern' => $r['pattern'],
            'name' => $r['name'],
        ], $this->routes);
    }

    private function normalize(string $path): string
    {
        $path = '/' . trim($path, '/');
        return $path === '//' ? '/' : $path;
    }

    private function match(string $pattern, string $path): ?array
    {
        $paramNames = [];
        $regex = preg_replace_callback('/\{(\*?)([a-zA-Z_][a-zA-Z0-9_]*)\}/', function ($m) use (&$paramNames) {
            $paramNames[] = $m[2];
            return $m[1] === '*' ? '(.+)' : '([^/]+)';
        }, $pattern);

        $regex = '#^' . $regex . '$#';
        if (!preg_match($regex, $path, $matches)) {
            return null;
        }

        array_shift($matches);
        return array_combine($paramNames, array_map('urldecode', $matches)) ?: [];
    }
}
