<?php
declare(strict_types=1);

namespace Infolab;

/**
 * Router minimaliste. Supporte les paramètres `:nom` dans les chemins
 * (capturés comme [^/]+) et marque chaque route comme protégée par token ou non.
 *
 * En cas de chemin connu sur une autre méthode -> 405 method_not_allowed.
 * En cas de chemin inconnu -> 404 not_found. Les deux passent par ApiException.
 */
final class Router
{
    /**
     * @var array<int,array{
     *   method:string,
     *   pattern:string,
     *   paramNames:array<int,string>,
     *   handler:array{0:class-string,1:string},
     *   auth:bool
     * }>
     */
    private array $routes = [];

    /** Enregistre une route GET (publique, pas de token). */
    public function get(string $path, array $handler): void
    {
        $this->add('GET', $path, $handler, false);
    }

    /** Enregistre une route POST (token requis). */
    public function post(string $path, array $handler): void
    {
        $this->add('POST', $path, $handler, true);
    }

    /** Enregistre une route PATCH (token requis). */
    public function patch(string $path, array $handler): void
    {
        $this->add('PATCH', $path, $handler, true);
    }

    /** Enregistre une route DELETE (token requis). */
    public function delete(string $path, array $handler): void
    {
        $this->add('DELETE', $path, $handler, true);
    }

    public function add(string $method, string $path, array $handler, bool $requiresAuth): void
    {
        // /api/projects/:slug -> /api/projects/([^/]+), avec ['slug'] dans paramNames.
        $paramNames = [];
        $pattern = preg_replace_callback(
            '#:([a-zA-Z_][a-zA-Z0-9_]*)#',
            static function (array $m) use (&$paramNames): string {
                $paramNames[] = $m[1];
                return '([^/]+)';
            },
            $path
        );

        $this->routes[] = [
            'method'     => $method,
            'pattern'    => '#^' . $pattern . '$#',
            'paramNames' => $paramNames,
            'handler'    => $handler,
            'auth'       => $requiresAuth,
        ];
    }

    /**
     * Cherche la route matchant méthode+chemin.
     *
     * @return array{0:array<string,mixed>,1:array<string,string>} couple (route, params nommés)
     * @throws \ApiException 405 si chemin connu sur une autre méthode, 404 sinon
     */
    public function dispatch(string $method, string $path): array
    {
        $methodAllowed = [];
        foreach ($this->routes as $route) {
            if (!preg_match($route['pattern'], $path, $m)) {
                continue;
            }
            if ($route['method'] !== $method) {
                $methodAllowed[$route['method']] = true;
                continue;
            }
            array_shift($m); // enlève le match complet
            $params = [];
            foreach ($route['paramNames'] as $i => $name) {
                $params[$name] = urldecode($m[$i]);
            }
            return [$route, $params];
        }

        if ($methodAllowed !== []) {
            throw new \ApiException(405, [
                'error'   => 'method_not_allowed',
                'allowed' => array_keys($methodAllowed),
            ]);
        }
        throw new \ApiException(404, ['error' => 'not_found']);
    }
}
