<?php

declare(strict_types=1);

namespace BotGuard\Core;

use BotGuard\Core\Exceptions\HttpException;

/**
 * Regex-based HTTP router.
 *
 * Routes are registered as `method + path pattern -> handler`, where the path
 * may contain `{param}` placeholders. Handlers are either a closure or the
 * `Controller@method` string form which the Kernel resolves via the container.
 * Per-route middleware names are stored and executed by the Kernel pipeline.
 */
final class Router
{
    /** @var array<int, array{method:string, regex:string, params:string[], handler:mixed, middleware:string[]}> */
    private array $routes = [];

    /** @var string[] Middleware applied to routes registered inside group(). */
    private array $groupMiddleware = [];

    private string $groupPrefix = '';

    public function get(string $path, mixed $handler, array $middleware = []): void
    {
        $this->add('GET', $path, $handler, $middleware);
    }

    public function post(string $path, mixed $handler, array $middleware = []): void
    {
        $this->add('POST', $path, $handler, $middleware);
    }

    public function put(string $path, mixed $handler, array $middleware = []): void
    {
        $this->add('PUT', $path, $handler, $middleware);
    }

    public function delete(string $path, mixed $handler, array $middleware = []): void
    {
        $this->add('DELETE', $path, $handler, $middleware);
    }

    /**
     * Register a batch of routes sharing a prefix and/or middleware.
     */
    public function group(array $attributes, callable $callback): void
    {
        $previousPrefix = $this->groupPrefix;
        $previousMiddleware = $this->groupMiddleware;

        $this->groupPrefix = $previousPrefix . ($attributes['prefix'] ?? '');
        $this->groupMiddleware = array_merge($previousMiddleware, (array) ($attributes['middleware'] ?? []));

        $callback($this);

        $this->groupPrefix = $previousPrefix;
        $this->groupMiddleware = $previousMiddleware;
    }

    public function add(string $method, string $path, mixed $handler, array $middleware = []): void
    {
        $path = $this->groupPrefix . $path;
        $path = '/' . trim($path, '/');
        $path = $path === '/' ? '/' : rtrim($path, '/');

        $params = [];
        $regex = preg_replace_callback(
            '#\{([a-zA-Z_][a-zA-Z0-9_]*)\}#',
            static function (array $m) use (&$params): string {
                $params[] = $m[1];

                return '([^/]+)';
            },
            $path
        );

        $this->routes[] = [
            'method'     => strtoupper($method),
            'regex'      => '#^' . $regex . '$#',
            'params'     => $params,
            'handler'    => $handler,
            'middleware' => array_merge($this->groupMiddleware, $middleware),
        ];
    }

    /**
     * Match a request to a route.
     *
     * @return array{handler:mixed, params:array<string,string>, middleware:string[]}
     *
     * @throws HttpException 404 when no path matches, 405 when only the method differs.
     */
    public function match(Request $request): array
    {
        $path = $request->path();
        $method = $request->method();
        $pathMatchedButMethodFailed = false;

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

            if ($route['method'] !== $method) {
                $pathMatchedButMethodFailed = true;

                continue;
            }

            array_shift($matches);
            $params = array_combine($route['params'], $matches) ?: [];

            return [
                'handler'    => $route['handler'],
                'params'     => $params,
                'middleware' => $route['middleware'],
            ];
        }

        throw new HttpException($pathMatchedButMethodFailed ? 405 : 404);
    }
}
