<?php

declare(strict_types=1);

namespace Core;

/**
 * Router — lightweight front-controller router.
 *
 * Routes are registered as:
 *   GET  /admin/menu          → [MenuController::class, 'index']
 *   POST /admin/menu/store    → [MenuController::class, 'store']
 *
 * URL parameters use {param} syntax:
 *   GET  /admin/menu/{id}/edit → [MenuController::class, 'edit']
 */
final class Router
{
    /** @var array<string, array<string, array{0: class-string, 1: string, 2: string[]}>> */
    private array $routes = [];

    private string $basePath = '';

    public function setBasePath(string $path): void
    {
        $this->basePath = rtrim($path, '/');
    }

    // ── Registration ─────────────────────────────────────────────────────────

    /** @param array{0: class-string, 1: string} $action */
    public function get(string $uri, array $action): void
    {
        $this->addRoute('GET', $uri, $action);
    }

    /** @param array{0: class-string, 1: string} $action */
    public function post(string $uri, array $action): void
    {
        $this->addRoute('POST', $uri, $action);
    }

    /** @param array{0: class-string, 1: string} $action */
    public function put(string $uri, array $action): void
    {
        $this->addRoute('PUT', $uri, $action);
    }

    /** @param array{0: class-string, 1: string} $action */
    public function delete(string $uri, array $action): void
    {
        $this->addRoute('DELETE', $uri, $action);
    }

    /** @param array{0: class-string, 1: string} $action */
    private function addRoute(string $method, string $uri, array $action): void
    {
        $pattern = $this->uriToPattern($uri);
        $this->routes[$method][$pattern] = [$action[0], $action[1], $this->extractParamNames($uri)];
    }

    // ── Dispatch ──────────────────────────────────────────────────────────────

    public function dispatch(): void
    {
        $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');

        // Support method override via hidden _method field (PUT / DELETE from HTML forms)
        if ($method === 'POST' && isset($_POST['_method'])) {
            $override = strtoupper($_POST['_method']);
            if (in_array($override, ['PUT', 'DELETE'], true)) {
                $method = $override;
            }
        }

        $uri = $this->currentUri();

        foreach ($this->routes[$method] ?? [] as $pattern => $route) {
            if (preg_match($pattern, $uri, $matches)) {
                [$controller, $methodName, $paramNames] = $route;

                // Build named params from regex captures
                $params = [];
                foreach ($paramNames as $i => $name) {
                    $params[$name] = $matches[$i + 1] ?? null;
                }

                $this->runController($controller, $methodName, $params);
                return;
            }
        }

        $this->notFound();
    }

    // ── Internals ─────────────────────────────────────────────────────────────

    private function currentUri(): string
    {
        $uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/';

        if ($this->basePath !== '' && str_starts_with($uri, $this->basePath)) {
            $uri = substr($uri, strlen($this->basePath));
        }

        return '/' . ltrim($uri, '/') ?: '/';
    }

    private function uriToPattern(string $uri): string
    {
        $escaped = preg_quote($uri, '#');
        // Replace escaped {param} placeholders with named capture groups
        $pattern = preg_replace('#\\\{(\w+)\\\}#', '([^/]+)', $escaped);
        return '#^' . $pattern . '$#';
    }

    /** @return string[] */
    private function extractParamNames(string $uri): array
    {
        preg_match_all('#\{(\w+)\}#', $uri, $matches);
        return $matches[1];
    }

    /**
     * @param class-string             $controller
     * @param array<string, mixed>     $params
     */
    private function runController(string $controller, string $method, array $params): void
    {
        if (!class_exists($controller)) {
            throw new \RuntimeException("Controller [{$controller}] not found.");
        }

        $instance = new $controller();

        if (!method_exists($instance, $method)) {
            throw new \RuntimeException("Method [{$method}] not found on [{$controller}].");
        }

        $instance->$method($params);
    }

    private function notFound(): void
    {
        http_response_code(404);
        if (file_exists(VIEW_PATH . '/errors/404.php')) {
            require VIEW_PATH . '/errors/404.php';
        } else {
            echo '<h1>404 — Page Not Found</h1>';
        }
    }
}
