<?php

namespace App\Core;

/**
 * Session — Session Lifecycle Manager
 *
 * Provides a centralised, type-safe wrapper around PHP's native session API.
 * Called by Bootstrap::init() (INIT-010 integration) before any request
 * processing, ensuring the session is active before Controllers, Middleware,
 * or CSRF validation attempt to read or write session data.
 *
 * Session configuration is read from config/session.php (INIT-002):
 *   name            → inveetaire_session
 *   save_path       → storage/sessions/
 *   cookie.httponly → true
 *   cookie.secure   → true (production) / false (local dev)
 *   cookie.samesite → Strict
 *   gc_maxlifetime  → 86400
 *
 * Security rules (ADR-005, DBP v1.0 Ch.6):
 *   - Session cookie is HttpOnly — JavaScript cannot access it.
 *   - Session cookie is SameSite=Strict — mitigates CSRF via cookie.
 *   - Session cookie is Secure in production — HTTPS only.
 *   - Session files are stored in storage/sessions/, not the system temp dir.
 *   - session_regenerate_id() must be called on privilege escalation (Auth module).
 *
 * Role-based timeout enforcement (AUTH-004):
 *   Three helper methods are added here for use by AuthMiddleware (AUTH-004):
 *     isInactivityExpired(int $seconds) — checks time since last activity
 *     isAbsoluteExpired(int $seconds)   — checks time since session creation
 *     touch()                           — stamps last_active_at on each request
 *
 *   Timeout configuration is read from config/session.php:
 *     session.inactivity_timeout — universal 2-hour inactivity limit
 *     session.timeout.{role}     — role-specific absolute session limits
 *
 * @package Inveetaire
 * @ref     DBP v1.0 Ch.6, ADR-005, INIT-010, AUTH-004
 */
class Session
{
    /**
     * Track whether start() has been called in this request.
     * Prevents duplicate session_start() calls if start() is invoked more than once.
     * @var bool
     */
    private static bool $started = false;

    // ─────────────────────────────────────────────────────────────────────────
    // LIFECYCLE
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Start the session with the configured security settings.
     *
     * Must be called exactly once per request, by Bootstrap::init().
     * Safe to call multiple times — subsequent calls are no-ops.
     *
     * Sequence:
     *   1. Skip if session already active (prevents headers-already-sent errors).
     *   2. Set session name from config.
     *   3. Set session save path (verified writable; falls back to system default).
     *   4. Configure GC (garbage collection) max lifetime.
     *   5. Apply cookie security parameters (httponly, secure, samesite, path, domain).
     *   6. Call session_start().
     *
     * @return void
     */
    public static function start(): void
    {
        if (self::$started || session_status() === PHP_SESSION_ACTIVE) {
            self::$started = true;
            return;
        }

        // ── 1. Session name ───────────────────────────────────────────────
        $name = (string) config('session.name', 'inveetaire_session');
        session_name($name);

        // ── 2. Save path ──────────────────────────────────────────────────
        $savePath = (string) config('session.save_path', '');

        if ($savePath === '') {
            // Fall back to storage/sessions/ relative to ROOT_PATH.
            $savePath = ROOT_PATH . DIRECTORY_SEPARATOR . 'storage'
                      . DIRECTORY_SEPARATOR . 'sessions';
        }

        if (!is_dir($savePath)) {
            // Attempt to create — may fail in restricted environments.
            @mkdir($savePath, 0750, true);
        }

        if (is_writable($savePath)) {
            session_save_path($savePath);
        } else {
            // Log and continue with system default — don't block the request.
            Logger::warning('Session save path is not writable; using system default.', [
                'path' => $savePath,
            ]);
        }

        // ── 3. GC lifetime ────────────────────────────────────────────────
        $gcMax = (int) config('session.gc_maxlifetime', 86400);
        ini_set('session.gc_maxlifetime', (string) $gcMax);
        ini_set('session.gc_probability', (string) (int) config('session.gc_probability', 1));
        ini_set('session.gc_divisor',     (string) (int) config('session.gc_divisor', 100));

        // ── 4. Cookie parameters ──────────────────────────────────────────
        $cookie = config('session.cookie', []);
        $cookie = is_array($cookie) ? $cookie : [];

        session_set_cookie_params([
            'lifetime' => 0,            // Session cookie — expires on browser close.
            'path'     => $cookie['path']   ?? '/',
            'domain'   => $cookie['domain'] ?? '',
            'secure'   => (bool) ($cookie['secure']   ?? false),
            'httponly' => (bool) ($cookie['httponly']  ?? true),
            'samesite' => (string) ($cookie['samesite'] ?? 'Strict'),
        ]);

        // ── 5. Start ──────────────────────────────────────────────────────
        session_start();
        self::$started = true;
    }

    /**
     * Regenerate the session ID.
     * Must be called on privilege escalation (login, role change) to prevent
     * session fixation attacks. Implemented by the Auth module (Sprint 1).
     *
     * @param bool $deleteOld  Delete the old session file. Default true.
     * @return void
     */
    public static function regenerate(bool $deleteOld = true): void
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_regenerate_id($deleteOld);
        }
    }

    /**
     * Destroy the session and clear all session data.
     * Used by the logout flow (Sprint 1 Auth module).
     *
     * @return void
     */
    public static function destroy(): void
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            $_SESSION = [];

            // Remove the session cookie from the browser.
            if (ini_get('session.use_cookies')) {
                $params = session_get_cookie_params();
                setcookie(
                    session_name(),
                    '',
                    time() - 42000,
                    $params['path'],
                    $params['domain'],
                    $params['secure'],
                    $params['httponly']
                );
            }

            session_destroy();
        }

        self::$started = false;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // TIMEOUT HELPERS (AUTH-004)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return true if the session's inactivity window has exceeded the limit.
     *
     * Compares the current time against the `last_active_at` session key.
     * That key is set by AuthService::bindSession() on login and refreshed
     * by Session::touch() on every authenticated request via AuthMiddleware.
     *
     * Returns false (not expired) when:
     *   - No active session exists.
     *   - `last_active_at` is absent (malformed session — safe-pass; touch() will
     *     set the key on the current request).
     *
     * @param int $seconds Maximum allowed idle time in seconds (e.g. 7200 = 2h).
     * @return bool True when the inactivity window has been exceeded.
     */
    public static function isInactivityExpired(int $seconds): bool
    {
        if (session_status() !== PHP_SESSION_ACTIVE || !isset($_SESSION['last_active_at'])) {
            return false;
        }

        return (time() - (int) $_SESSION['last_active_at']) > $seconds;
    }

    /**
     * Return true if the session's absolute lifetime has exceeded the limit.
     *
     * Compares the current time against the `login_at` session key.
     * That key is set once by AuthService::bindSession() at login and never
     * updated — it represents the session origin timestamp.
     *
     * Returns false (not expired) when:
     *   - No active session exists.
     *   - `login_at` is absent (malformed session — safe-pass).
     *
     * @param int $seconds Maximum session lifetime in seconds (e.g. 28800 = 8h).
     * @return bool True when the absolute session lifetime has been exceeded.
     */
    public static function isAbsoluteExpired(int $seconds): bool
    {
        if (session_status() !== PHP_SESSION_ACTIVE || !isset($_SESSION['login_at'])) {
            return false;
        }

        return (time() - (int) $_SESSION['login_at']) > $seconds;
    }

    /**
     * Update the `last_active_at` session key to the current timestamp.
     *
     * Called by AuthMiddleware on every successfully authenticated request
     * after all timeout checks have passed. This keeps the inactivity window
     * sliding forward with each request.
     *
     * No-op when the session is not active.
     *
     * @return void
     */
    public static function touch(): void
    {
        if (session_status() === PHP_SESSION_ACTIVE) {
            $_SESSION['last_active_at'] = time();
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DATA ACCESS
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Read a value from the session.
     *
     * @param string $key     Session key.
     * @param mixed  $default Fallback if key is absent.
     * @return mixed
     */
    public static function get(string $key, $default = null)
    {
        return $_SESSION[$key] ?? $default;
    }

    /**
     * Write a value to the session.
     *
     * @param string $key
     * @param mixed  $value
     * @return void
     */
    public static function set(string $key, $value): void
    {
        $_SESSION[$key] = $value;
    }

    /**
     * Check if a session key exists and is not null.
     *
     * @param string $key
     * @return bool
     */
    public static function has(string $key): bool
    {
        return isset($_SESSION[$key]);
    }

    /**
     * Remove a key from the session.
     *
     * @param string $key
     * @return void
     */
    public static function remove(string $key): void
    {
        unset($_SESSION[$key]);
    }

    /**
     * Return true if the session is currently active.
     *
     * @return bool
     */
    public static function isActive(): bool
    {
        return session_status() === PHP_SESSION_ACTIVE;
    }
}
