<?php

/**
 * Thin application helper — centralises bootstrap, auth gating, CSRF lifecycle,
 * flash messages, redirect, and template rendering.
 *
 * Eliminates the ~6-8 lines of identical preamble repeated across every page
 * and action handler. See REFACTORING_RECOMMENDATIONS.md §1 + §3.
 */
class App
{
    private static bool $booted = false;

    // ── Bootstrap ─────────────────────────────────────────────────────────────

    /**
     * Boot once per request: load Database, ensure CSRF token exists.
     * Suitable for public pages (no auth required).
     */
    public static function boot(): Database
    {
        if (!self::$booted) {
            require_once APP_ROOT . '/src/Database.php';
            self::$booted = true;
        }
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        self::ensureCsrf();
        return Database::getInstance();
    }

    /**
     * Gate for admin pages: require auth + CSRF token + load Database.
     */
    public static function adminGuard(): Database
    {
        require_once APP_ROOT . '/src/AdminAuth.php';
        AdminAuth::requireLogin();
        return self::boot();
    }

    // ── CSRF lifecycle ────────────────────────────────────────────────────────

    /**
     * Ensure a CSRF token exists in the session.
     * Called automatically by boot() / adminGuard().
     */
    private static function ensureCsrf(): void
    {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
    }

    /**
     * Validate the CSRF token on a POST request.
     * Halts with 403 if the token is missing or invalid.
     */
    public static function verifyCsrf(): void
    {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST'
            || !isset($_POST['csrf_token'], $_SESSION['csrf_token'])
            || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
            http_response_code(403);
            exit('CSRF token invalide.');
        }
    }

    /**
     * Regenerate the CSRF token after a successful mutation.
     */
    public static function rotateCsrf(): void
    {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }

    // ── Flash messages ────────────────────────────────────────────────────────

    /**
     * Store a flash message in the session.
     *
     * @param 'success'|'error'|'warning' $type
     */
    public static function flash(string $type, string $message): void
    {
        $_SESSION["_flash_{$type}"] = $message;
    }

    /**
     * Store the name of the field that should receive autofocus after a
     * validation failure (WCAG 3.3.1).
     */
    public static function flashAutofocus(string $fieldName): void
    {
        $_SESSION['_flash_autofocus'] = $fieldName;
    }

    /**
     * Consume and return the autofocus field name, then clear it.
     * Returns null when no autofocus hint is present.
     */
    public static function consumeAutofocus(): ?string
    {
        $field = $_SESSION['_flash_autofocus'] ?? null;
        unset($_SESSION['_flash_autofocus']);
        return $field;
    }

    /**
     * Consume and return flash messages, then clear them from the session.
     *
     * @return array{error: ?string, success: ?string, warning: ?string}
     */
    public static function consumeFlash(): array
    {
        $error   = $_SESSION['_flash_error']   ?? null;
        $success = $_SESSION['_flash_success'] ?? null;
        $warning = $_SESSION['_flash_warning'] ?? null;

        unset($_SESSION['_flash_error'], $_SESSION['_flash_success'], $_SESSION['_flash_warning']);

        // Note: autofocus is consumed separately via consumeAutofocus().
        return ['error' => $error, 'success' => $success, 'warning' => $warning];
    }

    // ── Redirect ──────────────────────────────────────────────────────────────

    /**
     * Flash a message and redirect. Terminates the script.
     */
    public static function redirect(string $url, ?string $success = null, ?string $error = null): never
    {
        if ($success !== null) {
            self::flash('success', $success);
        }
        if ($error !== null) {
            self::flash('error', $error);
        }
        header('Location: ' . $url);
        exit;
    }

    // ── Asset versioning ─────────────────────────────────────────────────────

    /**
     * Return an asset URL with a filemtime-based cache-busting query string.
     * Input is a root-relative URL path (e.g. /assets/css/main.css).
     */
    public static function assetV(string $urlPath): string
    {
        $file = APP_ROOT . '/public' . $urlPath;
        $v    = file_exists($file) ? filemtime($file) : 0;
        return $urlPath . ($v ? '?v=' . $v : '');
    }

    // ── Template rendering ────────────────────────────────────────────────────

    /**
     * Render a full page: head → header → content template → footer.
     *
     * Expects $vars to contain the same keys the templates already rely on
     * ($pageTitle, $bodyClass, $isAdmin, $extraCss, $ogTags, etc.).
     * The footer variant (public vs admin) is chosen automatically based
     * on $isAdmin.
     *
     * @param string $template  Path relative to APP_ROOT/templates/
     * @param array  $vars      Variables to expose inside the templates
     */
    public static function render(string $template, array $vars = []): void
    {
        // Make all vars available in the template scope.
        extract($vars);

        include APP_ROOT . '/templates/head.php';
        include APP_ROOT . '/templates/header.php';
        include APP_ROOT . '/templates/' . $template;

        if (!empty($isAdmin)) {
            include APP_ROOT . '/templates/admin/footer.php';
        } else {
            include APP_ROOT . '/templates/footer.php';
        }
    }
}
