<?php

declare(strict_types=1);

/**
 * ACC Standalone Router
 */

if (!isset($GLOBALS['wp_rest_routes']) || !is_array($GLOBALS['wp_rest_routes'])) {
    $GLOBALS['wp_rest_routes'] = [];
}

if (!isset($GLOBALS['acc_rest_routes']) || !is_array($GLOBALS['acc_rest_routes'])) {
    $GLOBALS['acc_rest_routes'] = [];
}

/**
 * Dispatch current HTTP request.
 */
function acc_router_dispatch(): void
{
    acc_router_send_default_headers();

    $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
    $path   = acc_router_get_path();

    if ($method === 'OPTIONS') {
        http_response_code(204);
        exit;
    }

    // Global abuse guard (per-IP). Auth/OTP endpoints have tighter buckets separately.
    if (function_exists('acc_rate_limit') && function_exists('acc_client_ip')) {
        $ip = acc_client_ip();
        $max = (function_exists('acc_is_local_environment') && acc_is_local_environment()) ? 2400 : 600;
        if (!acc_rate_limit('api_global_' . $ip, $max, 60)) {
            acc_json_response([
                'success' => false,
                'code'    => 'rate_limited',
                'message' => 'تعداد درخواست‌ها زیاد است. کمی بعد تلاش کنید.',
            ], 429);
        }
    }

    $match = acc_router_match_route($method, $path);
    if ($match === null) {
        acc_json_response([
            'code'    => 'rest_no_route',
            'message' => 'مسیر درخواستی یافت نشد.',
            'data'    => [
                'status' => 404,
                'path'   => $path,
                'method' => $method,
                'routes_registered' => defined('ACC_DEBUG') && ACC_DEBUG
                    ? count(acc_router_get_registered_routes())
                    : null,
            ],
        ], 404);
    }

    if (!empty($match['method_not_allowed'])) {
        acc_json_response([
            'code'    => 'rest_method_not_allowed',
            'message' => 'Method not allowed for this route.',
            'data'    => [
                'status' => 405,
                'path'   => $path,
                'method' => $method,
            ],
        ], 405);
    }

    $endpoint  = $match['endpoint'];
    $urlParams = acc_router_extract_url_params($match['matches']);

    try {
        $request = acc_build_request($method, $path, $urlParams);
        acc_router_apply_route_request_metadata($request, $endpoint, $urlParams);

        $permissionCallback = $endpoint['permission_callback'] ?? '__return_true';
        $permissionResult = acc_call_callback($permissionCallback, [$request]);

        if ($permissionResult instanceof WP_Error) {
            acc_router_output_response(rest_ensure_response($permissionResult));
        }

        if ($permissionResult === false || $permissionResult === null) {
            acc_json_response([
                'code'    => 'rest_forbidden',
                'message' => 'You do not have permission to access this endpoint.',
                'data'    => ['status' => 403],
            ], 403);
        }

        if (!isset($endpoint['callback'])) {
            acc_json_response([
                'code'    => 'missing_callback',
                'message' => 'Route callback is not defined.',
                'data'    => ['status' => 500],
            ], 500);
        }

        $result = acc_call_callback($endpoint['callback'], [$request]);
        acc_router_output_response(rest_ensure_response($result));

    } catch (Throwable $e) {
        acc_json_response([
            'code'    => 'internal_server_error',
            'message' => defined('ACC_DEBUG') && ACC_DEBUG
                ? $e->getMessage()
                : 'Internal server error',
            'data'    => [
                'status' => 500,
                'file'   => defined('ACC_DEBUG') && ACC_DEBUG ? $e->getFile() : null,
                'line'   => defined('ACC_DEBUG') && ACC_DEBUG ? $e->getLine() : null,
                'path'   => defined('ACC_DEBUG') && ACC_DEBUG ? $path : null,
                'method' => defined('ACC_DEBUG') && ACC_DEBUG ? $method : null,
            ],
        ], 500);
    }
}

/**
 * Build route index once after controllers register routes.
 */
function acc_route_compile_index(): void
{
    if (!empty($GLOBALS['acc_route_index_compiled'])) {
        return;
    }

    $routes = acc_router_get_registered_routes();
    $index  = [];

    foreach ($routes as $routeKey => $routeDef) {
        if (!is_string($routeKey) || $routeKey === '') {
            continue;
        }

        $regex     = acc_route_to_regex($routeKey);
        $parts     = explode('/', trim($routeKey, '/'));
        $bucket    = count($parts) >= 3 ? '/' . implode('/', array_slice($parts, 0, 3)) : $routeKey;
        $endpoints = acc_router_get_route_endpoints($routeDef);

        foreach ($endpoints as $endpoint) {
            if (!is_array($endpoint)) {
                continue;
            }
            foreach (acc_normalize_methods($endpoint['methods'] ?? 'GET') as $httpMethod) {
                $index[$httpMethod][$bucket][] = [
                    'regex'    => $regex,
                    'endpoint' => $endpoint,
                ];
            }
        }
    }

    $GLOBALS['acc_route_index']          = $index;
    $GLOBALS['acc_route_index_compiled'] = true;
}

/**
 * Fast route match using compiled index (fallback to full scan).
 *
 * @return array{endpoint: array, matches: array}|null
 */
function acc_router_match_route(string $method, string $path): ?array
{
    $method = strtoupper($method);
    $index  = $GLOBALS['acc_route_index'] ?? [];
    $pathMatched = false;

    if (!empty($index)) {
        $parts   = explode('/', trim($path, '/'));
        $buckets = [];
        if (count($parts) >= 3) {
            $buckets[] = '/' . implode('/', array_slice($parts, 0, 3));
        }
        $buckets[] = '*';

        foreach ($buckets as $bucket) {
            $candidates = $bucket === '*'
                ? acc_router_index_flat($index[$method] ?? [])
                : ($index[$method][$bucket] ?? []);

            foreach ($candidates as $candidate) {
                $matches = [];
                if (!preg_match($candidate['regex'], $path, $matches)) {
                    continue;
                }
                return [
                    'endpoint' => $candidate['endpoint'],
                    'matches'  => $matches,
                ];
            }

            // path may match another HTTP method on same regex
            foreach ($index as $httpMethod => $methodBuckets) {
                if ($httpMethod === $method) {
                    continue;
                }
                $otherCandidates = $bucket === '*'
                    ? acc_router_index_flat($methodBuckets)
                    : ($methodBuckets[$bucket] ?? []);
                foreach ($otherCandidates as $candidate) {
                    if (preg_match($candidate['regex'], $path)) {
                        $pathMatched = true;
                        break 2;
                    }
                }
            }
        }

        if ($pathMatched) {
            return ['method_not_allowed' => true];
        }
    }

    $legacy = acc_router_match_route_legacy($method, $path);
    if ($legacy !== null) {
        return $legacy;
    }

    if (acc_router_path_exists_for_other_method($method, $path)) {
        return ['method_not_allowed' => true];
    }

    return null;
}

function acc_router_path_exists_for_other_method(string $method, string $path): bool
{
    $index = $GLOBALS['acc_route_index'] ?? [];
    foreach ($index as $httpMethod => $buckets) {
        if ($httpMethod === $method) {
            continue;
        }
        foreach (acc_router_index_flat($buckets) as $candidate) {
            if (preg_match($candidate['regex'], $path)) {
                return true;
            }
        }
    }
    return false;
}

/**
 * @param array<string, array<int, array>> $byBucket
 * @return array<int, array>
 */
function acc_router_index_flat(array $byBucket): array
{
    $flat = [];
    foreach ($byBucket as $items) {
        foreach ($items as $item) {
            $flat[] = $item;
        }
    }
    return $flat;
}

/**
 * Legacy linear scan (fallback).
 *
 * @return array{endpoint: array, matches: array}|null
 */
function acc_router_match_route_legacy(string $method, string $path): ?array
{
    $routes = acc_router_get_registered_routes();

    foreach ($routes as $routeKey => $routeDef) {
        $routePattern = acc_router_get_route_pattern($routeKey, $routeDef);
        if ($routePattern === '') {
            continue;
        }

        $regex = acc_route_to_regex($routePattern);
        if (!preg_match($regex, $path, $matches)) {
            continue;
        }

        foreach (acc_router_get_route_endpoints($routeDef) as $endpoint) {
            if (in_array($method, acc_normalize_methods($endpoint['methods'] ?? 'GET'), true)) {
                return [
                    'endpoint' => $endpoint,
                    'matches'  => $matches,
                ];
            }
        }

        return null;
    }

    return null;
}

/**
 * Return registered routes from the active compatibility layer.
 */
function acc_router_get_registered_routes(): array
{
    if (
        isset($GLOBALS['wp_rest_server'])
        && is_object($GLOBALS['wp_rest_server'])
        && method_exists($GLOBALS['wp_rest_server'], 'get_routes')
    ) {
        $serverRoutes = $GLOBALS['wp_rest_server']->get_routes();
        if (is_array($serverRoutes) && !empty($serverRoutes)) {
            return $serverRoutes;
        }
    }

    if (isset($GLOBALS['wp_rest_routes']) && is_array($GLOBALS['wp_rest_routes']) && !empty($GLOBALS['wp_rest_routes'])) {
        return $GLOBALS['wp_rest_routes'];
    }

    if (isset($GLOBALS['acc_rest_routes']) && is_array($GLOBALS['acc_rest_routes']) && !empty($GLOBALS['acc_rest_routes'])) {
        return acc_router_legacy_routes_to_wp_routes($GLOBALS['acc_rest_routes']);
    }

    return [];
}

/**
 * Convert legacy acc_rest_routes list format to keyed WP-like route format.
 */
function acc_router_legacy_routes_to_wp_routes(array $legacyRoutes): array
{
    $routes = [];

    foreach ($legacyRoutes as $routeDef) {
        if (!is_array($routeDef)) {
            continue;
        }

        $namespace = trim((string)($routeDef['namespace'] ?? ''), '/');
        $route = '/' . ltrim((string)($routeDef['route'] ?? ''), '/');
        $fullRoute = '/' . $namespace . $route;
        $fullRoute = '/' . trim($fullRoute, '/');

        if ($fullRoute === '/') {
            continue;
        }

        $routes[$fullRoute] = [
            [
                'methods' => $routeDef['methods'] ?? 'GET',
                'callback' => $routeDef['callback'] ?? null,
                'permission_callback' => $routeDef['permission_callback'] ?? '__return_true',
                'args' => $routeDef['args'] ?? [],
            ],
        ];
    }

    return $routes;
}

/**
 * Resolve route pattern from route key/definition.
 */
function acc_router_get_route_pattern($routeKey, $routeDef): string
{
    if (is_string($routeKey) && $routeKey !== '') {
        return '/' . trim($routeKey, '/');
    }

    if (is_array($routeDef)) {
        $namespace = trim((string)($routeDef['namespace'] ?? ''), '/');
        $route = '/' . ltrim((string)($routeDef['route'] ?? ''), '/');
        return '/' . trim($namespace . $route, '/');
    }

    return '';
}


/**
 * Normalize route definition to endpoint list.
 */
function acc_router_get_route_endpoints($routeDef): array
{
    if (!is_array($routeDef)) {
        return [];
    }

    if (isset($routeDef['endpoints']) && is_array($routeDef['endpoints'])) {
        return $routeDef['endpoints'];
    }

    $looksLikeSingleEndpoint =
        array_key_exists('callback', $routeDef)
        || array_key_exists('methods', $routeDef)
        || array_key_exists('permission_callback', $routeDef);

    if ($looksLikeSingleEndpoint) {
        return [$routeDef];
    }

    $endpoints = [];

    foreach ($routeDef as $maybeEndpoint) {
        if (is_array($maybeEndpoint)) {
            $endpoints[] = $maybeEndpoint;
        }
    }

    return $endpoints;
}

/**
 * Extract named URL parameters from preg_match results.
 */
function acc_router_extract_url_params(array $matches): array
{
    $urlParams = [];

    foreach ($matches as $key => $value) {
        if (!is_int($key)) {
            $urlParams[$key] = $value;
        }
    }

    return $urlParams;
}

/**
 * Add route metadata to request object if supported.
 */
function acc_router_apply_route_request_metadata(WP_REST_Request $request, array $endpoint, array $urlParams): void
{
    if (method_exists($request, 'set_url_params')) {
        $request->set_url_params($urlParams);
    }

    if (method_exists($request, 'set_attributes')) {
        $attributes = [
            'args' => $endpoint['args'] ?? [],
            'url_params' => $urlParams,
        ];

        $request->set_attributes($attributes);
    }
}

/**
 * Build WP_REST_Request-like object from globals.
 */
function acc_build_request(string $method, string $path, array $urlParams = []): WP_REST_Request
{
    $request = new WP_REST_Request($method, $path);

    if (method_exists($request, 'set_url_params')) {
        $request->set_url_params($urlParams);
    }

    if (method_exists($request, 'set_query_params')) {
        $queryParams = $_GET;
        unset($queryParams['rest_route'], $queryParams['debug_routes']);
        $request->set_query_params($queryParams);
    }

    $rawBody = $GLOBALS['acc_raw_body'] ?? null;
    if ($rawBody === null) {
        $rawBody = file_get_contents('php://input') ?: '';
        $GLOBALS['acc_raw_body'] = $rawBody;
    }
    if (method_exists($request, 'set_body')) {
        $request->set_body($rawBody ?: '');
    }

    $contentType = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
    $jsonParams = [];

    if ($rawBody && stripos($contentType, 'application/json') !== false) {
        $decoded = json_decode($rawBody, true);
        if (is_array($decoded)) {
            $jsonParams = $decoded;
        }
    }

    if (method_exists($request, 'set_json_params')) {
        $request->set_json_params($jsonParams);
    }

    $bodyParams = $_POST;

    if (empty($bodyParams) && in_array($method, ['PUT', 'PATCH', 'DELETE'], true)) {
        parse_str($rawBody ?: '', $parsedBody);
        if (is_array($parsedBody)) {
            $bodyParams = $parsedBody;
        }
    }

    if (method_exists($request, 'set_body_params')) {
        $request->set_body_params($bodyParams);
    }

    if (!empty($_FILES) && method_exists($request, 'set_file_params')) {
        $request->set_file_params($_FILES);
    }

    if (method_exists($request, 'set_headers')) {
        $request->set_headers(acc_router_get_request_headers());
    }

    if (method_exists($request, 'set_attributes')) {
        $request->set_attributes([
            'url_params'   => $urlParams,
            'query_params' => $_GET,
            'body_params'  => $bodyParams,
            'json_params'  => $jsonParams,
        ]);
    }

    return $request;
}

/**
 * Extract request headers.
 */
function acc_router_get_request_headers(): array
{
    if (function_exists('getallheaders')) {
        $headers = getallheaders();
        if (is_array($headers)) {
            return $headers;
        }
    }

    $headers = [];

    foreach ($_SERVER as $key => $value) {
        if (strpos($key, 'HTTP_') !== 0) {
            continue;
        }

        $name = substr($key, 5);
        $name = str_replace('_', '-', strtolower($name));
        $name = ucwords($name, '-');

        $headers[$name] = $value;
    }

    if (isset($_SERVER['CONTENT_TYPE'])) {
        $headers['Content-Type'] = $_SERVER['CONTENT_TYPE'];
    }

    if (isset($_SERVER['CONTENT_LENGTH'])) {
        $headers['Content-Length'] = $_SERVER['CONTENT_LENGTH'];
    }

    return $headers;
}

/**
 * Extract route path from current request.
 *
 * Supports:
 * - /acc-engine/api.php/acc/v1/acl/me
 * - /acc-engine/api.php?rest_route=/acc/v1/acl/me
 * - /acc-engine/index.php?rest_route=/acc/v1/acl/me
 */
function acc_router_get_path(): string
{
    if (!empty($_GET['rest_route'])) {
        return acc_router_normalize_path((string)$_GET['rest_route']);
    }

    $uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
    $uriPath = is_string($uriPath) ? $uriPath : '/';
    $uriPath = str_replace('\\', '/', $uriPath);

    $scriptName = str_replace('\\', '/', $_SERVER['SCRIPT_NAME'] ?? '');
    $phpSelf = str_replace('\\', '/', $_SERVER['PHP_SELF'] ?? '');

    if ($scriptName !== '' && strpos($uriPath, $scriptName) === 0) {
        return acc_router_normalize_path(substr($uriPath, strlen($scriptName)));
    }

    if ($phpSelf !== '' && strpos($uriPath, $phpSelf) === 0) {
        return acc_router_normalize_path(substr($uriPath, strlen($phpSelf)));
    }

    foreach (['api.php', 'index.php'] as $frontController) {
        $marker = '/' . $frontController . '/';
        $pos = strpos($uriPath, $marker);

        if ($pos !== false) {
            return acc_router_normalize_path(substr($uriPath, $pos + strlen('/' . $frontController)));
        }
    }

    return acc_router_normalize_path($uriPath);
}

/**
 * Normalize path.
 */
function acc_router_normalize_path(string $path): string
{
    $path = parse_url($path, PHP_URL_PATH);
    $path = is_string($path) ? $path : '/';
    $path = rawurldecode($path);
    $path = str_replace('\\', '/', $path);
    $path = '/' . trim($path, '/');
    $path = preg_replace('#/+#', '/', $path) ?: '/';

    return $path;
}

/**
 * Convert WP-style route pattern to regex.
 */
/**
 * Convert WP-style route pattern to safe regex.
 *
 * Supports route patterns like:
 * /acc/v1/products
 * /acc/v1/products/(?P<id>\d+)
 */
function acc_route_to_regex(string $route): string
{
    // حذف اسلش‌های ابتدا و انتهای روت برای ایجاد الگوی یکدست
    $route = '/' . trim($route, '/');

    if ($route === '/' || $route === '') {
        return '#^/?$#u';
    }

    $tokens = [];
    $index = 0;

    // استخراج موقت تمام تگ‌های ریجکس مانند (?P<id>\d+)
    $routeWithTokens = preg_replace_callback(
        '#\(\?P<([a-zA-Z_][a-zA-Z0-9_]*)>([^)]*)\)#',
        static function (array $matches) use (&$tokens, &$index): string {
            $token = '___ACC_ROUTE_PARAM_' . $index . '___';
            $tokens[$token] = '(?P<' . $matches[1] . '>' . $matches[2] . ')';
            $index++;
            return $token;
        },
        $route
    );

    if (!is_string($routeWithTokens)) {
        $routeWithTokens = $route;
    }

    // فرار دادن (Escape) کاراکترهای خاص در بخش‌های ثابت روت
    $quoted = preg_quote($routeWithTokens, '#');

    // بازگرداندن تگ‌های ریجکس به جای توکن‌های موقت
    foreach ($tokens as $token => $regexPart) {
        $quoted = str_replace(preg_quote($token, '#'), $regexPart, $quoted);
    }

    // مطابقت دادن آدرس ورودی با یا بدون اسلش انتهایی
    return '#^' . $quoted . '/?$#u';
}



/**
 * Normalize methods to uppercase array.
 */
function acc_normalize_methods($methods): array
{
    if (is_array($methods)) {
        $normalized = [];

        foreach ($methods as $method) {
            $method = strtoupper(trim((string)$method));

            if ($method !== '') {
                $normalized[] = $method;
            }
        }

        return array_values(array_unique($normalized));
    }

    if (is_string($methods)) {
        $parts = preg_split('/[,\|]+/', $methods) ?: [];
        $normalized = [];

        foreach ($parts as $method) {
            $method = strtoupper(trim((string)$method));

            if ($method !== '') {
                $normalized[] = $method;
            }
        }

        return array_values(array_unique($normalized));
    }

    return ['GET'];
}

/**
 * Execute callback safely.
 *
 * Supports:
 * - function_name
 * - Closure
 * - ['ClassName', 'method']
 * - [$object, 'method']
 * - ClassName::method
 */
function acc_call_callback($callback, array $args = [])
{
    if (is_string($callback) && strpos($callback, '::') !== false) {
        $callback = explode('::', $callback, 2);
    }

    if (is_array($callback) && count($callback) === 2) {
        [$target, $method] = $callback;

        if (is_string($target) && class_exists($target) && method_exists($target, (string)$method)) {
            return $target::{$method}(...$args);
        }

        if (is_object($target) && method_exists($target, (string)$method)) {
            return $target->{$method}(...$args);
        }
    }

    if (!is_callable($callback)) {
        throw new RuntimeException('Invalid route callback.');
    }

    return call_user_func_array($callback, $args);
}

/**
 * Send final response.
 */
function acc_router_output_response($response): void
{
    if ($response instanceof WP_Error) {
        $response = rest_ensure_response($response);
    }

    if ($response instanceof WP_REST_Response) {
        $status = $response->get_status();
        $data = $response->get_data();
        $headers = $response->get_headers();

        if (!headers_sent()) {
            http_response_code($status);

            $hasContentType = false;

            foreach ($headers as $key => $value) {
                if (strtolower((string)$key) === 'content-type') {
                    $hasContentType = true;
                }

                if (is_array($value)) {
                    foreach ($value as $singleValue) {
                        header($key . ': ' . $singleValue, false);
                    }
                } else {
                    header($key . ': ' . $value);
                }
            }

            if (!$hasContentType) {
                header('Content-Type: application/json; charset=utf-8');
            }

            if (defined('ACC_START_TIME')) {
                $elapsed = round((microtime(true) - ACC_START_TIME) * 1000, 2);
                header('X-ACC-Response-Ms: ' . $elapsed);
            }
        }

        echo wp_json_encode($data, defined('ACC_JSON_FLAGS') ? ACC_JSON_FLAGS : JSON_UNESCAPED_UNICODE);
        exit;
    }

    acc_json_response($response, 200);
}

/**
 * Default headers.
 */
function acc_router_send_default_headers(): void
{
    if (headers_sent()) {
        return;
    }

    // CORS allowlist (never wildcard * in production) — see standalone/security.php
    if (function_exists('acc_send_cors_headers')) {
        acc_send_cors_headers();
    } elseif (function_exists('acc_is_local_environment') && acc_is_local_environment()) {
        header('Access-Control-Allow-Origin: http://localhost');
        header('Vary: Origin');
        header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
        header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Cart-Session');
    }

    header('Content-Type: application/json; charset=utf-8');
    if (function_exists('acc_send_security_headers')) {
        acc_send_security_headers(true);
    }
}
