<?php

namespace EasyLinkAutomations\HTTP\Router;

if (!defined('ABSPATH')) {
    exit;
}

use EasyLinkAutomations\HTTP\Request\Request;
use EasyLinkAutomations\HTTP\Response;

/**
 * Route registration and dispatch engine.
 *
 * Supports two request types:
 *  - ajax : registers WordPress wp_ajax_* hooks
 *  - api  : registers WordPress REST API routes via register_rest_route()
 */
final class Router
{
    private string $type;
    private string $prefix;
    private string $version;

    /** @var array<int, array{method: string, path: string, handler: array, middlewares: string[]}> */
    private array $routes = [];

    /** @var array<string, class-string> */
    private array $middlewareMap = [];

    public function __construct(string $type, string $prefix, string $version)
    {
        $this->type    = $type;
        $this->prefix  = $prefix;
        $this->version = $version;

        Route::setRouter($this);
    }

    /**
     * Register available middleware classes (name => class).
     *
     * @param array<string, class-string> $middlewares
     */
    public function setMiddlewares(array $middlewares): void
    {
        $this->middlewareMap = $middlewares;
    }

    /**
     * Add a route definition to the collection.
     */
    public function addRoute(string $method, string $path, array $handler): RouteEntry
    {
        $index          = count($this->routes);
        $this->routes[] = [
            'method'      => $method,
            'path'        => $path,
            'handler'     => $handler,
            'middlewares' => [],
        ];

        return new RouteEntry($this, $index);
    }

    /**
     * Open a route group.
     *
     * Snapshot the current route count, execute the closure (which registers
     * routes), then return a RouteGroup that knows which routes were added.
     */
    public function group(\Closure $fn): RouteGroup
    {
        $startIndex = count($this->routes);
        $fn();
        $endIndex = count($this->routes);

        return new RouteGroup($this, $startIndex, $endIndex);
    }

    /**
     * Attach middleware names to routes in a specific index range.
     *
     * @param string[] $names
     */
    public function applyMiddlewaresToRange(int $start, int $end, array $names): void
    {
        for ($i = $start; $i < $end; $i++) {
            if (isset($this->routes[$i])) {
                $this->routes[$i]['middlewares'] = array_merge(
                    $this->routes[$i]['middlewares'],
                    $names
                );
            }
        }
    }

    /**
     * Attach middleware to a single route by index.
     *
     * @param string[] $names
     */
    public function applyMiddlewaresToIndex(int $index, array $names): void
    {
        if (isset($this->routes[$index])) {
            $this->routes[$index]['middlewares'] = array_merge(
                $this->routes[$index]['middlewares'],
                $names
            );
        }
    }

    /**
     * Register all collected routes with WordPress.
     */
    public function register(): void
    {
        if ($this->type === 'ajax') {
            $this->registerAjaxRoutes();
        } elseif ($this->type === 'api') {
            $this->registerRestRoutes();
        }
    }

    // ──────────────────────────────────────────────
    //  AJAX Registration
    // ──────────────────────────────────────────────

    private function registerAjaxRoutes(): void
    {
        // Read the current AJAX action to support dynamic path-param routes.
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Only reads 'action' for route matching; nonce is verified in AuthMiddleware before controller dispatch.
        $currentAction = isset($_POST['action'])
            ? sanitize_text_field(wp_unslash($_POST['action']))
            : '';

        foreach ($this->routes as $route) {
            if ($this->pathHasParams($route['path'])) {
                // Dynamic route: only register if the current request matches
                $params = $this->matchPathParams($currentAction, $this->prefix . $route['path']);
                if ($params !== null) {
                    $actionName     = $currentAction;
                    $capturedRoute  = $route;
                    $capturedParams = $params;

                    add_action('wp_ajax_' . $actionName, function () use ($capturedRoute, $capturedParams) {
                        $this->dispatch($capturedRoute, $capturedParams);
                    });
                }
            } else {
                // Static route
                $actionName    = $this->prefix . $route['path'];
                $capturedRoute = $route;

                add_action('wp_ajax_' . $actionName, function () use ($capturedRoute) {
                    $this->dispatch($capturedRoute, []);
                });
            }
        }
    }

    // ──────────────────────────────────────────────
    //  REST API Registration
    // ──────────────────────────────────────────────

    private function registerRestRoutes(): void
    {
        $namespace = $this->prefix . ($this->version ? '/' . $this->version : '');

        foreach ($this->routes as $route) {
            $restPath      = $this->pathToRestPattern($route['path']);
            $capturedRoute = $route;

            register_rest_route($namespace, '/' . $restPath, [
                'methods'             => $route['method'],
                'callback'            => function (\WP_REST_Request $wpRequest) use ($capturedRoute) {
                    return $this->dispatchRest($capturedRoute, $wpRequest);
                },
                'permission_callback' => function () use ($capturedRoute) {
                    return $this->authorizeRestRequest($capturedRoute);
                },
            ]);
        }
    }

    /**
     * Authorize REST route access.
     *
     * Secure by default: REST routes are denied unless they explicitly declare
     * middleware. Route middleware can include a special `public` marker to
     * allow unauthenticated access for endpoints like OAuth callbacks.
     */
    private function authorizeRestRequest(array $route): bool|\WP_Error
    {
        $middlewares = $route['middlewares'] ?? [];

        if (in_array('public', $middlewares, true)) {
            return true;
        }

        if (empty($middlewares)) {
            return new \WP_Error('rest_forbidden', 'Access denied', ['status' => 403]);
        }

        $request = new Request();

        foreach ($middlewares as $middlewareName) {
            if ($middlewareName === 'public') {
                continue;
            }

            $middlewareClass = $this->middlewareMap[$middlewareName] ?? null;
            if (!$middlewareClass || !class_exists($middlewareClass)) {
                return new \WP_Error('rest_forbidden', 'Access denied', ['status' => 403]);
            }

            $middleware = new $middlewareClass();
            $result     = $this->invokeMiddlewareHandle($middleware, $request);

            if ($result instanceof Response) {
                return new \WP_Error(
                    'rest_forbidden',
                    $result->getMessage() ?: 'Access denied',
                    ['status' => $result->getStatusCode() ?: 403]
                );
            }
        }

        return true;
    }

    // ──────────────────────────────────────────────
    //  Dispatch — AJAX
    // ──────────────────────────────────────────────

    private function dispatch(array $route, array $pathParams): void
    {
        $request = new Request();

        // Run middleware chain
        foreach ($route['middlewares'] as $middlewareName) {
            $middlewareClass = $this->middlewareMap[$middlewareName] ?? null;
            if (!$middlewareClass || !class_exists($middlewareClass)) {
                continue;
            }

            $middleware = new $middlewareClass();
            $result     = $this->invokeMiddlewareHandle($middleware, $request);

            if ($result instanceof Response) {
                $result->send();

                return;
            }
        }

        // Invoke controller method
        [$controllerClass, $method] = $route['handler'];
        $controller = new $controllerClass();
        $response   = $controller->{$method}($request, ...$pathParams);

        $response->send();
    }

    // ──────────────────────────────────────────────
    //  Dispatch — REST API
    // ──────────────────────────────────────────────

    private function dispatchRest(array $route, \WP_REST_Request $wpRequest): \WP_REST_Response
    {
        $request = new Request();

        // Extract URL path params from WP_REST_Request (in declaration order)
        $pathParams = array_values($wpRequest->get_url_params());

        [$controllerClass, $method] = $route['handler'];
        $controller = new $controllerClass();
        $response   = $controller->{$method}($request, ...$pathParams);

        return $response->toRestResponse();
    }

    /**
     * Invoke middleware `handle` with backward compatibility for signatures
     * that do or do not accept a Request parameter.
     */
    private function invokeMiddlewareHandle(object $middleware, Request $request): Response|bool
    {
        $handle = new \ReflectionMethod($middleware, 'handle');

        return $handle->getNumberOfParameters() > 0
            ? $middleware->handle($request)
            : $middleware->handle();
    }

    // ──────────────────────────────────────────────
    //  Path Utilities
    // ──────────────────────────────────────────────

    private function pathHasParams(string $path): bool
    {
        return str_contains($path, '{');
    }

    /**
     * Try to match a concrete action string against a parameterised pattern.
     *
     * Example:
     *   pattern = "EASYLINK_AUTOMATIONS_triggers/{slug}"
     *   action  = "EASYLINK_AUTOMATIONS_triggers/contact-form-7"
     *   returns = ['contact-form-7']
     *
     * @return string[]|null  Extracted values, or null if no match.
     */
    private function matchPathParams(string $action, string $pattern): ?array
    {
        // Split on {param} placeholders, quote each literal segment, then rejoin
        // with a capture group. preg_quote must NOT run on the whole pattern
        // first, because it would escape { and } making them unmatchable.
        $parts  = preg_split('/\{[^}]+\}/', $pattern);
        $quoted = array_map(fn (string $p) => preg_quote($p, '#'), $parts);
        $regex  = implode('([^/]+)', $quoted);

        if (!preg_match('#^' . $regex . '$#', $action, $matches)) {
            return null;
        }

        array_shift($matches); // Remove full-match entry

        return $matches;
    }

    /**
     * Convert "triggers/{slug}" → "triggers/(?P<slug>[^/]+)" for REST routes.
     */
    private function pathToRestPattern(string $path): string
    {
        return preg_replace('/\{([^}]+)\}/', '(?P<$1>[^/]+)', $path);
    }
}
