<?php

namespace App\Core;

/**
 * Router — HTTP Request Router
 *
 * Matches an incoming HTTP request against registered route patterns
 * and dispatches to the appropriate controller action.
 *
 * Supported capabilities (INIT-007 scope, DBP v1.0 Ch.5):
 *   - HTTP methods: GET, POST only (no PUT/DELETE at MVP)
 *   - Named URL parameters: {id}, {slug}, {guest_code}, etc.
 *   - Route groups with shared path prefix and middleware list
 *   - Dispatch to fully-qualified ControllerClass@method strings
 *   - 404 response via ErrorHandler for unmatched routes
 *   - Loading route files from the routes/ directory
 *
 * NOT in scope for INIT-007:
 *   - Middleware execution → INIT-009 (MiddlewarePipeline)
 *   - Authentication/Authorization → Sprint 1 (Auth module)
 *   - Request validation → Module-level services
 *
 * Middleware list per route is registered and STORED during INIT-007.
 * The MiddlewarePipeline (INIT-009) reads this list and executes it.
 * The Router does NOT execute middleware — it only records the intent.
 *
 * @package Inveetaire
 * @ref     DBP v1.0 Ch.5, SAD v1.1 Ch.4, INIT-007
 */
class Router
{
    // ─────────────────────────────────────────────────────────────────────────
    // ROUTE STORAGE
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * All registered routes.
     *
     * Each entry: [
     *   'method'     => string       — 'GET' | 'POST'
     *   'pattern'    => string       — URI pattern e.g. '/app/couple/{id}'
     *   'action'     => string       — 'FullyQualifiedClass@method'
     *   'middleware' => string[]     — ordered middleware class names
     * ]
     *
     * @var array<int, array{method:string, pattern:string, action:string, middleware:string[]}>
     */
    private array $routes = [];

    // ─────────────────────────────────────────────────────────────────────────
    // GROUP STATE (used during route file loading)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Current path prefix applied by an open group() call.
     * Nested groups concatenate: '/app' + '/couple' = '/app/couple'.
     * @var string
     */
    private string $groupPrefix = '';

    /**
     * Current middleware stack inherited from open group() calls.
     * Per-route middleware is appended AFTER group middleware.
     * @var string[]
     */
    private array $groupMiddleware = [];

    // ─────────────────────────────────────────────────────────────────────────
    // ROUTE REGISTRATION
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Register a GET route.
     *
     * @param string   $path       URI pattern. May contain {param} placeholders.
     * @param string   $action     'FullyQualifiedClass@method' — e.g.
     *                             'App\Modules\Auth\AuthController@showLogin'
     * @param string[] $middleware Ordered list of middleware class names.
     *                             Executed before the action by MiddlewarePipeline (INIT-009).
     * @return void
     */
    public function get(string $path, string $action, array $middleware = []): void
    {
        $this->addRoute('GET', $path, $action, $middleware);
    }

    /**
     * Register a POST route.
     *
     * @param string   $path       URI pattern.
     * @param string   $action     'FullyQualifiedClass@method'
     * @param string[] $middleware Ordered middleware class names.
     * @return void
     */
    public function post(string $path, string $action, array $middleware = []): void
    {
        $this->addRoute('POST', $path, $action, $middleware);
    }

    /**
     * Define a route group with a shared path prefix and/or middleware.
     *
     * Groups may be nested. Nested group prefixes are concatenated.
     * Nested group middleware arrays are merged (parent first).
     *
     * Example:
     *   $router->group('/app/couple', function ($router) {
     *       $router->get('/dashboard', 'CoupleDashboardController@index', []);
     *       // Registers: GET /app/couple/dashboard
     *   }, ['App\Middleware\AuthMiddleware', 'App\Middleware\RoleMiddleware:couple']);
     *
     * @param string   $prefix     Path prefix to prepend to all routes inside.
     * @param callable $callback   Receives $this (the Router) as its argument.
     * @param string[] $middleware Middleware applied to all routes in this group.
     * @return void
     */
    public function group(string $prefix, callable $callback, array $middleware = []): void
    {
        // Save state before entering the group.
        $parentPrefix     = $this->groupPrefix;
        $parentMiddleware = $this->groupMiddleware;

        // Merge group settings.
        $this->groupPrefix     = $parentPrefix . $prefix;
        $this->groupMiddleware = array_merge($parentMiddleware, $middleware);

        // Execute the callback. All get()/post() calls inside inherit the group settings.
        $callback($this);

        // Restore state after the group closes (supports nested groups).
        $this->groupPrefix     = $parentPrefix;
        $this->groupMiddleware = $parentMiddleware;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE — Route Storage
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Store a route definition after applying the current group context.
     *
     * @param string   $method     HTTP method.
     * @param string   $path       Route-relative URI pattern.
     * @param string   $action     Controller@method string.
     * @param string[] $middleware Per-route middleware (appended after group middleware).
     * @return void
     */
    private function addRoute(string $method, string $path, string $action, array $middleware): void
    {
        $this->routes[] = [
            'method'     => $method,
            'pattern'    => $this->groupPrefix . $path,
            'action'     => $action,
            'middleware' => array_merge($this->groupMiddleware, $middleware),
        ];
    }

    // ─────────────────────────────────────────────────────────────────────────
    // ROUTE FILE LOADING
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Require all route files from the routes/ directory.
     *
     * Each file receives the $router variable (this instance).
     * Route files contain only route registrations — no business logic.
     *
     * Load order matters for specificity: more specific routes should be
     * in files loaded earlier. Current order: public → couple → crew → admin → support.
     *
     * @param string $routesDirectory Absolute path to the routes/ directory.
     * @return void
     */
    public function loadRoutes(string $routesDirectory): void
    {
        $router = $this; // Passed into each route file as $router.

        $files = [
            'public',    // Guest-facing pages, invitation links (Sprint 4)
            'couple',    // Couple portal (Sprint 1+)
            'crew',      // Crew portal (Sprint 1+)
            'admin',     // Super Admin portal (Sprint 1+)
            'support',   // Support mode (Sprint 5+)
            'developer', // Developer Console (DX001+)
        ];

        $dir = rtrim($routesDirectory, '/\\');

        foreach ($files as $name) {
            $filePath = $dir . DIRECTORY_SEPARATOR . $name . '.php';

            if (file_exists($filePath)) {
                require $filePath; // $router variable is available in each file.
            } else {
                Logger::warning("Route file missing: $filePath");
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DISPATCH
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Match the request against all registered routes and invoke the action.
     *
     * If no route matches: returns a 404 Not Found response via ErrorHandler.
     *
     * Middleware execution:
     *   The matched route's middleware list is stored on the request via
     *   setMiddleware() — NOT executed here. The MiddlewarePipeline (INIT-009)
     *   wraps this dispatch call and executes middleware before the action.
     *   For INIT-007 (before INIT-009 exists), dispatch is direct.
     *
     * @param Request $request The incoming HTTP request.
     * @return void
     */
    public function dispatch(Request $request): void
    {
        $method = $request->getMethod();
        $path   = $request->getPath();

        foreach ($this->routes as $route) {

            // HTTP method must match.
            if ($route['method'] !== $method) {
                continue;
            }

            // Attempt to match the URI pattern.
            $params = $this->matchPattern($route['pattern'], $path);

            if ($params === null) {
                continue;
            }

            // Route matched — inject named parameters into the request.
            $request->setParams($params);

            // Execute the middleware stack, then invoke the controller action.
            // MiddlewarePipeline (INIT-009) wraps the action with the route's middleware list.
            // If $route['middleware'] is empty, the action is called directly.
            $action = $route['action'];

            $pipeline = new MiddlewarePipeline($route['middleware']);
            $pipeline->run($request, function (Request $req) use ($action): void {
                $this->invoke($action, $req);
            });
            return;
        }

        // No route matched the request.
        ErrorHandler::renderNotFound();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE — Pattern Matching
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Match a URI against a route pattern and extract named parameters.
     *
     * Conversion: {param} → (?P<param>[^/]+)
     *   {id}         matches any non-slash sequence, captured as 'id'
     *   {slug}       matches any non-slash sequence, captured as 'slug'
     *   {guest_code} matches any non-slash sequence, captured as 'guest_code'
     *
     * @param string $pattern Route pattern e.g. '/app/couple/{id}/guests'
     * @param string $uri     Application-relative URI path e.g. '/app/couple/42/guests'
     *
     * @return array<string, string>|null  Named captures on match; null on no match.
     */
    private function matchPattern(string $pattern, string $uri): ?array
    {
        // Replace {param} placeholders with named PCRE groups.
        // Param names: letters, digits, underscores; must start with letter or underscore.
        $regex = preg_replace(
            '/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/',
            '(?P<$1>[^/]+)',
            $pattern
        );

        // Anchor the pattern to the full URI string.
        $regex = '#^' . $regex . '$#';

        if (!preg_match($regex, $uri, $matches)) {
            return null;
        }

        // Return only the string-keyed captures (named groups).
        // preg_match() fills numeric AND named keys — we only want named.
        $params = [];
        foreach ($matches as $key => $value) {
            if (is_string($key)) {
                $params[$key] = $value;
            }
        }

        return $params;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE — Action Invocation
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Invoke a controller action.
     *
     * Supported action format:
     *   'Fully\Qualified\ControllerClass@methodName'
     *   e.g. 'App\Modules\Auth\AuthController@showLogin'
     *
     * The controller class is resolved via the PSR-4 Autoloader (INIT-003).
     * No DI container. No Reflection. The controller is instantiated with
     * new $class() and the method is called with the Request as the only argument.
     *
     * @param string  $action  'ClassName@method' string.
     * @param Request $request The current HTTP request (passed to the method).
     * @return void
     */
    private function invoke(string $action, Request $request): void
    {
        if (strpos($action, '@') === false) {
            Logger::error('Invalid route action — missing @: ' . $action);
            ErrorHandler::renderNotFound();
            return;
        }

        [$class, $method] = explode('@', $action, 2);

        // Autoloader resolves the class. class_exists() triggers it.
        if (!class_exists($class)) {
            Logger::error('Controller class not found', ['class' => $class, 'method' => $method]);
            ErrorHandler::renderNotFound();
            return;
        }

        if (!method_exists($class, $method)) {
            Logger::error('Controller method not found', ['class' => $class, 'method' => $method]);
            ErrorHandler::renderNotFound();
            return;
        }

        $controller = new $class();
        $controller->$method($request);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DEBUG UTILITY
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return all registered routes (for debugging and testing only).
     * Not to be called in production request paths.
     *
     * @return array<int, array{method:string, pattern:string, action:string, middleware:string[]}>
     */
    public function getRoutes(): array
    {
        return $this->routes;
    }
}
