<?php

declare(strict_types=1);

namespace Fennec\Routing;

/**
 * Fait correspondre un chemin et une méthode HTTP à une route enregistrée.
 */
final class Router
{
    /**
     * @param list<RouteDefinition> $routes
     */
    public function __construct(private readonly array $routes)
    {
    }

    /**
     * @throws RouteNotFoundException Si aucune route ne correspond au chemin.
     * @throws MethodNotAllowedException Si le chemin correspond mais pas la méthode HTTP.
     */
    public function match(string $path, string $method): RouteMatch
    {
        $method = \strtoupper($method);
        $allowedMethods = [];

        foreach ($this->routes as $route) {
            $pattern = $this->compilePattern($route->path);

            if (\preg_match($pattern, $path, $matches) !== 1) {
                continue;
            }

            $routeMethods = \array_map(\strtoupper(...), $route->methods);

            if (!\in_array($method, $routeMethods, true)) {
                $allowedMethods = [...$allowedMethods, ...$routeMethods];
                continue;
            }

            return new RouteMatch(
                $route->controllerClass,
                $route->controllerMethod,
                $this->extractParameters($matches),
            );
        }

        if ($allowedMethods !== []) {
            throw new MethodNotAllowedException($path, $method, \array_values(\array_unique($allowedMethods)));
        }

        throw new RouteNotFoundException($path);
    }

    private function compilePattern(string $path): string
    {
        $segments = \explode('/', $path);

        $regexSegments = \array_map(
            static function (string $segment): string {
                if (\preg_match('#^\{([a-zA-Z_][a-zA-Z0-9_]*)\}$#', $segment, $matches) === 1) {
                    return \sprintf('(?P<%s>[^/]+)', $matches[1]);
                }

                return \preg_quote($segment, '#');
            },
            $segments,
        );

        return '#^' . \implode('/', $regexSegments) . '$#';
    }

    /**
     * @param array<int|string, string> $matches
     *
     * @return array<string, string>
     */
    private function extractParameters(array $matches): array
    {
        $parameters = [];

        foreach ($matches as $key => $value) {
            if (\is_string($key)) {
                $parameters[$key] = $value;
            }
        }

        return $parameters;
    }
}
