<?php
/**
 * Slate — public router.
 *
 * Maps clean public URLs (e.g. /forms/<slug>, /book/...) to plugin
 * handlers. Plugins register their public prefixes via the
 * `public_routes` filter.
 *
 * Slate is otherwise a direct-file-routing app. The public router
 * is opt-in: anything actually present on disk is still served by
 * Apache directly (admin/, customer/, plugins/<slug>/storefront/
 * for legacy plugins like Shop). Only paths that don't resolve to
 * a file fall through to public.php, which delegates here.
 *
 * Route registration (in a plugin's boot()):
 *
 *   Hook::addFilter('public_routes', function (array $routes): array {
 *       $routes['forms'] = [
 *           'handler' => plugin_dir('forms') . '/public/router.php',
 *           // Optional: 'methods' => ['GET', 'POST']
 *       ];
 *       return $routes;
 *   });
 *
 * The handler receives:
 *   - $_GET['_route_prefix']  — the matched prefix (e.g. "forms")
 *   - $_GET['_route_path']    — the remainder (e.g. "contact-us")
 *
 * The longest-prefix-match wins, so a plugin registering "forms/admin"
 * could intercept "forms/admin/foo" without breaking "forms/<slug>".
 */

class PublicRouter {

    /**
     * Dispatch the public path. Renders a 404 if no plugin claims
     * the prefix.
     */
    public static function dispatch(string $path): void {
        $path = trim($path, '/');

        // Defensive: reject anything that tries to climb out of the
        // public URL space. The handler is required to be an absolute
        // file path so a malicious path can't traverse into it, but
        // strip ".." segments anyway.
        $path = preg_replace('#(^|/)\.\.(/|$)#', '/', (string)$path);
        $path = ltrim((string)$path, '/');

        $routes = Hook::applyFilters('public_routes', []);
        if (!is_array($routes)) $routes = [];

        $best = null; // [prefix, route]
        foreach ($routes as $prefix => $route) {
            $prefix = trim((string)$prefix, '/');
            if ($prefix === '' || !is_array($route)) continue;
            if ($path === $prefix || str_starts_with($path . '/', $prefix . '/')) {
                if ($best === null || strlen($prefix) > strlen($best[0])) {
                    $best = [$prefix, $route];
                }
            }
        }

        if ($best === null) {
            self::renderNotFound($path);
            return;
        }

        [$prefix, $route] = $best;
        $handler = (string)($route['handler'] ?? '');
        if ($handler === '' || !is_file($handler)) {
            slate_log("PublicRouter: prefix '{$prefix}' is registered but handler is missing: {$handler}", 'error');
            self::renderNotFound($path);
            return;
        }

        // Method gate (optional)
        if (!empty($route['methods']) && is_array($route['methods'])) {
            $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
            $allowed = array_map('strtoupper', $route['methods']);
            if (!in_array($method, $allowed, true)) {
                http_response_code(405);
                header('Allow: ' . implode(', ', $allowed));
                echo '405 Method Not Allowed';
                return;
            }
        }

        $remainder = substr($path, strlen($prefix));
        $remainder = ltrim($remainder, '/');

        $_GET['_route_prefix'] = $prefix;
        $_GET['_route_path']   = $remainder;

        require $handler;
    }

    /**
     * The list of public prefixes Slate knows about. Useful for
     * plugins that want to surface a list of public links in the
     * admin (e.g. "here are your public URLs").
     */
    public static function knownPrefixes(): array {
        $routes = Hook::applyFilters('public_routes', []);
        return is_array($routes) ? array_keys($routes) : [];
    }

    private static function renderNotFound(string $path): void {
        http_response_code(404);
        // Keep this minimal — no DB calls, no plugin code, no template
        // engine. A 404 page should be cheap and never break further.
        $title = 'Not found';
        $url   = htmlspecialchars($path, ENT_QUOTES, 'UTF-8');
        $home  = htmlspecialchars(defined('SLATE_URL') ? SLATE_URL : '/', ENT_QUOTES, 'UTF-8');
        echo "<!doctype html><meta charset=\"utf-8\"><title>{$title}</title>"
           . "<style>body{font:14px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;"
           . "background:#F5F4F1;color:#111217;display:grid;place-items:center;min-height:100vh;margin:0;padding:24px}"
           . ".card{background:#fff;border:1px solid rgba(0,0,0,0.08);border-radius:12px;padding:32px;max-width:480px;text-align:center}"
           . "h1{margin:0 0 8px;font-size:22px}p{margin:0 0 16px;color:#6B7280}"
           . "a{color:#2563EB;text-decoration:none}a:hover{color:#1D4ED8;text-decoration:underline}"
           . "code{font-family:ui-monospace,'SF Mono',Menlo,monospace;background:#FAFAF7;padding:1px 6px;border-radius:4px;font-size:12px}</style>"
           . "<div class=\"card\"><h1>Not found</h1>"
           . "<p>No route is registered for <code>/{$url}</code>.</p>"
           . "<p><a href=\"{$home}/\">Return home</a></p></div>";
    }
}
