<?php
// components/Router.php
namespace Components;

class Router
{
    /**
     * Routes registry
     * Structure: [routeName => ['path' => string, 'options' => array]]
     */
    private static array $routes = [];

    private static string $baseUrl = '';

    /** Common URL parameters to sync across all routes (e.g., ['tab']) */
    private static array $urlSyncCommon = [];

    // Defaults for route parameter names
    private const DEFAULT_TAB_PARAM = 'nav';      // main navigation only
    private const DEFAULT_SUBTAB_PARAM = 'subtab';      // main navigation only
    private const DEFAULT_PAGE_PARAM = 'action';    // partial/page loads

    // Optional: set a base URL if absolute URLs are required
    public static function setBaseUrl(string $baseUrl): void
    {
        self::$baseUrl = rtrim($baseUrl, '/');
    }

    /**
     * Get the base URL (protocol + host + optional directory) from the environment or configured base URL.
     *
     * @param bool $includeDirectory Whether to include the current script's directory.
     * @return string
     */
    public static function getBaseUrl(bool $includeDirectory = true): string
    {
        if (self::$baseUrl !== '') {
            return self::$baseUrl;
        }

        $protocol = 'http';
        if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
            $protocol = 'https';
        } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
            $protocol = 'https';
        } elseif (!empty($_SERVER['HTTP_CF_VISITOR'])) {
            $visitor = json_decode($_SERVER['HTTP_CF_VISITOR'], true);
            if (isset($visitor['scheme']) && $visitor['scheme'] === 'https') {
                $protocol = 'https';
            }
        }
        $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
        $baseUrl = $protocol . '://' . $host;

        if ($includeDirectory) {
            $script = $_SERVER['SCRIPT_NAME'] ?? '/';
            $dir = rtrim(dirname($script), '/\\');
            if ($dir !== '') {
                $baseUrl .= $dir;
            }
        }

        return $baseUrl;
    }

    /**
     * Make a relative URL absolute using the detected base URL and current directory.
     *
     * @param string $url
     * @return string
     */
    public static function makeAbsolute(string $url): string
    {
        if ($url === '' || strpos($url, '://') !== false) {
            return $url;
        }

        if (strpos($url, '/') === 0) {
            return self::getBaseUrl(false) . $url;
        }

        return self::getBaseUrl(true) . '/' . ltrim($url, '/');
    }

    /**
     * Join a base URL and a path safely, ensuring exactly one slash between them.
     *
     * @param string $base
     * @param string $path
     * @return string
     */
    public static function joinUrl(string $base, string $path): string
    {
        if ($path === '') {
            return $base;
        }

        // If path starts with query or fragment, just append
        if ($path[0] === '?' || $path[0] === '#') {
            return $base . $path;
        }

        return rtrim($base, '/') . '/' . ltrim($path, '/');
    }

    /**
     * Normalize a URL by collapsing accidental duplicate slashes in the path
     * (e.g. produced by concatenating a base that already ends with '/' and a
     * path that starts with '/', like 'm/' . '/api/...' => 'm//api/...').
     *
     * - The scheme separator '://' (http://, https://, webcal://, ...) is preserved.
     * - Query string and fragment are left untouched so encoded values are safe.
     *
     * @param string $url
     * @return string
     */
    public static function normalizeUrl(string $url): string
    {
        if ($url === '') {
            return $url;
        }

        // Split off query/fragment so we never touch encoded values there.
        $suffix = '';
        $cut = strcspn($url, '?#');
        if ($cut < strlen($url)) {
            $suffix = substr($url, $cut);
            $url = substr($url, 0, $cut);
        }

        // Collapse duplicate slashes in the path while preserving the scheme separator (://).
        $url = preg_replace('#(?<!:)//+#', '/', $url);

        return $url . $suffix;
    }

    /** Get the configured main navigation parameter name (defaults to 'tab'). */
    public static function subtabParam(): string
    {
        // Allow override via global constant
        if (defined('MB_PARAM_SUBTAB')) {
            $v = (string)constant('MB_PARAM_SUBTAB');
            if ($v !== '') return $v;
        }
        return self::DEFAULT_SUBTAB_PARAM;
    }    /** Get the configured main navigation parameter name (defaults to 'tab'). */
    public static function tabParam(): string
    {
        // Allow override via global constant
        if (defined('MB_PARAM_TAB')) {
            $v = (string)constant('MB_PARAM_TAB');
            if ($v !== '') return $v;
        }
        return self::DEFAULT_TAB_PARAM;
    }

    /** Get the configured partial/page load parameter name (defaults to 'page'). */
    public static function pageParam(): string
    {
        // Allow override via global constant
        if (defined('MB_PARAM_PAGE')) {
            $v = (string)constant('MB_PARAM_PAGE');
            if ($v !== '') return $v;
        }
        return self::DEFAULT_PAGE_PARAM;
    }

    /**
     * Register a named route
     *
     * @param string $name  Logical route name
     * @param string $path  Relative path to file (e.g., 'pages/dashboard.php')
     * @param array  $options Optional options, supported keys:
     *   - 'urlSyncParams' => array of query parameter names that should be synced to keep the browser URL in sync with the iframe URL (preferred)
     *   - 'urlParams' => same as above (deprecated; kept for backwards compatibility)
     *   - 'capability' => capability definition checked via Components\Capabilities::isSupported()
     *   - 'capabilities' => list of capability definitions, all must pass
     *   - 'requires' => alias for 'capability'
     *   - 'fallbackPath' => route file path to use when capability check fails
     *   - 'unsupportedPath' => alias for 'fallbackPath'
     */
    public static function add(string $name, string $path, array $options = []): void
    {
        self::$routes[$name] = [
            'path' => ltrim($path, '/'),
            'options' => $options,
        ];
    }

    // Generate a URL by route name with optional query params
    // Refactored: all routes go through the configured entry point (BEEZUI_ENTRYPOINT)
    public static function url(string $name, array $query = [], bool $absolute = false): string
    {
        // Choose the correct route parameter based on partial intent
        $routeParam = (isset($query['partial']) && (string)$query['partial'] === '1')
            ? self::pageParam()
            : self::tabParam();

        // Base query with <routeParam>=<route>
        $params = array_merge([$routeParam => $name], $query);

        // Determine entrypoint and merge any predefined query parameters from it
        $entry = defined('BEEZUI_ENTRYPOINT') ? (string)constant('BEEZUI_ENTRYPOINT') : 'app.php';
        $entryPath = $entry;
        $entryQsParams = [];
        if (strpos($entry, '?') !== false) {
            [$entryPath, $entryQs] = explode('?', $entry, 2);
            if ($entryQs !== '') {
                parse_str($entryQs, $entryQsParams);
                if (!is_array($entryQsParams)) {
                    $entryQsParams = [];
                }
            }
        }
        // Merge entrypoint query params first so explicit $query overrides them
        if ($entryQsParams) {
            $params = array_merge($entryQsParams, $params);
        }

        // Optionally enforce a required entry parameter (e.g., 'main') if defined
        if (defined('BEEZUI_ENTRYPOINT_REQUIRED_PARAM')) {
            $req = (string)constant('BEEZUI_ENTRYPOINT_REQUIRED_PARAM');
            if ($req !== '' && !array_key_exists($req, $params)) {
                $params[$req] = 1;
            }
        }

        $q = $params ? ('?' . http_build_query($params)) : '';
        $base = $absolute ? (self::getBaseUrl() . '/') : '';
        return $base . $entryPath . $q;
    }

    // Reverse lookup by path (useful for active navigation)
    public static function nameForPath(string $path): ?string
    {
        $path = ltrim($path, '/');
        foreach (self::$routes as $name => $def) {
            $p = is_array($def) ? ($def['path'] ?? '') : (string)$def;
            if ($p === $path) return $name;
        }
        return null;
    }

    /**
     * Resolve a route name to its registered relative file path, or null if not found.
     */
    public static function path(string $name): ?string
    {
        $def = self::$routes[$name] ?? null;
        if (!$def) return null;

        if (is_array($def)) {
            $primaryPath = isset($def['path']) ? (string)$def['path'] : null;
            if ($primaryPath === null || $primaryPath === '') {
                return null;
            }

            $options = isset($def['options']) && is_array($def['options']) ? $def['options'] : [];
            if (!self::isRouteSupported($options)) {
                $fallbackPath = self::unsupportedPath($options);
                if ($fallbackPath !== null) {
                    return $fallbackPath;
                }
            }

            return $primaryPath;
        }

        return is_string($def) ? $def : null;
    }

    /**
     * @param array<string,mixed> $options
     */
    private static function isRouteSupported(array $options): bool
    {
        if (array_key_exists('capabilities', $options)) {
            return Capabilities::isSupported(['all' => $options['capabilities']]);
        }
        if (array_key_exists('capability', $options)) {
            return Capabilities::isSupported($options['capability']);
        }
        if (array_key_exists('requires', $options)) {
            return Capabilities::isSupported($options['requires']);
        }
        return true;
    }

    /**
     * @param array<string,mixed> $options
     */
    private static function unsupportedPath(array $options): ?string
    {
        $value = $options['fallbackPath'] ?? $options['unsupportedPath'] ?? null;
        if (!is_string($value) || $value === '') {
            return null;
        }
        return ltrim($value, '/');
    }

    /**
     * Set common URL parameters that are always synced across routes
     */
    public static function setUrlSyncCommon(array $common): void
    {
        // normalize to unique, string-only values
        self::$urlSyncCommon = array_values(array_unique(array_map('strval', $common)));
    }

    /**
     * Get current URL sync configuration combining common and per-route parameters
     * Returns: ['common' => [...], 'routeParams' => ['routeName' => [...], ...]]
     */
    public static function getUrlSyncConfig(): array
    {
        $routeParams = [];
        foreach (self::$routes as $name => $def) {
            $opts = is_array($def) ? ($def['options'] ?? []) : [];
            $params = [];
            $params = isset($opts['urlSyncParams']) && is_array($opts['urlSyncParams']) ? array_values(array_unique(array_map('strval', $opts['urlSyncParams']))) : [];

            if ($params) {
                $routeParams[$name] = $params;
            }
        }
        return [
            'common' => self::$urlSyncCommon,
            'routeParams' => $routeParams,
        ];
    }
}
