<?php

declare(strict_types=1);

namespace App\Core;

use RuntimeException;

/**
 * Simple Router
 *
 * Handles HTTP routing with support for GET/POST methods,
 * route parameters, middleware stacks, and 404 handling.
 */
class Router
{
    /**
     * Registered routes
     *
     * @var array<string, array{handler: string, middleware: array<string>}>
     */
    private array $routes = [];

    /**
     * Named middleware groups
     *
     * @var array<string, callable>
     */
    private array $middleware = [];

    /**
     * Register a middleware by name.
     *
     * @param string $name The middleware name
     * @param callable $callback The middleware handler
     * @return void
     */
    public function addMiddleware(string $name, callable $callback): void
    {
        $this->middleware[$name] = $callback;
    }

    /**
     * Register a GET route.
     *
     * @param string $path The URI pattern (e.g. '/courses/{id}')
     * @param string $handler Controller@method string
     * @param array<string> $middleware Middleware names to apply
     * @return void
     */
    public function get(string $path, string $handler, array $middleware = []): void
    {
        $this->addRoute('GET', $path, $handler, $middleware);
    }

    /**
     * Register a POST route.
     *
     * @param string $path The URI pattern
     * @param string $handler Controller@method string
     * @param array<string> $middleware Middleware names to apply
     * @return void
     */
    public function post(string $path, string $handler, array $middleware = []): void
    {
        $this->addRoute('POST', $path, $handler, $middleware);
    }

    /**
     * Add a route to the internal routes array.
     *
     * Converts the route path into a regex pattern for matching,
     * capturing named parameters like {id} or {slug}.
     *
     * @param string $method The HTTP method (GET, POST)
     * @param string $path The URI pattern
     * @param string $handler Controller@method string
     * @param array<string> $middleware Middleware names to apply
     * @return void
     */
    private function addRoute(string $method, string $path, string $handler, array $middleware): void
    {
        // Convert route parameters like {id} into named regex capture groups
        $pattern = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', '(?P<$1>[^/]+)', $path);

        if ($pattern === null) {
            throw new RuntimeException("Failed to compile route pattern for: {$path}");
        }

        // Anchor the pattern to match the full path
        $pattern = '#^' . $pattern . '$#';

        $key = strtoupper($method) . ':' . $path;

        $this->routes[$key] = [
            'method'     => strtoupper($method),
            'pattern'    => $pattern,
            'handler'    => $handler,
            'middleware' => $middleware,
        ];
    }

    /**
     * Dispatch the incoming request to the appropriate controller method.
     *
     * Matches the URI and HTTP method against registered routes,
     * runs the middleware stack, extracts parameters, and calls
     * the controller method. Falls back to a 404 handler if no
     * matching route is found.
     *
     * @param string $uri The request URI (path only, no query string)
     * @param string $method The HTTP method (GET, POST)
     * @return void
     */
    public function dispatch(string $uri, string $method): void
    {
        // Strip query string and normalize URI
        $uri = '/' . trim(parse_url($uri, PHP_URL_PATH) ?? '/', '/');
        $uri = $uri !== '/' ? rtrim($uri, '/') : '/';

        $method = strtoupper($method);

        foreach ($this->routes as $route) {
            if ($route['method'] !== $method) {
                continue;
            }

            if (!preg_match($route['pattern'], $uri, $matches)) {
                continue;
            }

            // Extract named parameters (filter out integer keys)
            $params = array_filter(
                $matches,
                fn($key) => is_string($key),
                ARRAY_FILTER_USE_KEY
            );

            // Run middleware stack
            foreach ($route['middleware'] as $mw) {
                if (!isset($this->middleware[$mw])) {
                    throw new RuntimeException("Middleware '{$mw}' is not registered.");
                }

                ($this->middleware[$mw])();
            }

            // Parse and invoke the handler
            $this->invokeHandler($route['handler'], $params);

            return;
        }

        // No route matched — send 404
        $this->handleNotFound();
    }

    /**
     * Invoke a route handler (Controller@method).
     *
     * Splits the handler string, validates the format, instantiates
     * the controller, and calls the method with route parameters.
     *
     * @param string $handler The Controller@method string
     * @param array<string, string> $params Route parameters
     * @return void
     * @throws RuntimeException If the handler format is invalid
     */
    private function invokeHandler(string $handler, array $params): void
    {
        $parts = explode('@', $handler);

        if (count($parts) !== 2) {
            throw new RuntimeException(
                "Invalid route handler '{$handler}'. Expected 'Controller@method' format."
            );
        }

        [$controllerClass, $method] = $parts;

        if (!class_exists($controllerClass)) {
            throw new RuntimeException("Controller class '{$controllerClass}' not found.");
        }

        $controller = new $controllerClass();

        if (!method_exists($controller, $method)) {
            throw new RuntimeException(
                "Method '{$method}' does not exist on controller '{$controllerClass}'."
            );
        }

        $controller->{$method}($params);
    }

    /**
     * Handle a 404 Not Found response.
     *
     * Sends a 404 status code and renders a simple error page.
     * Can be overridden or customized to use a view template.
     *
     * @return never
     */
    private function handleNotFound(): never
    {
        http_response_code(404);
        header('Content-Type: text/html; charset=utf-8');

        // Check for a custom 404 view
        $notFoundView = APP_ROOT . '/app/Views/errors/404.php';

        if (file_exists($notFoundView)) {
            require $notFoundView;
        } else {
            echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
               . '<title>404 - Page Not Found</title>'
               . '<style>body{font-family:sans-serif;text-align:center;padding:5rem;}'
               . 'h1{font-size:3rem;color:#e74c3c;}</style></head>'
               . '<body><h1>404</h1><p>The page you requested could not be found.</p>'
               . '<a href="/">Return to Home</a></body></html>';
        }

        exit;
    }
}
