<?php

namespace App\Core;

/**
 * Csrf — CSRF Token Generation and Validation
 *
 * Provides a per-session CSRF token that protects all state-changing
 * (POST) requests from Cross-Site Request Forgery attacks.
 *
 * Token lifecycle (DBP v1.0 Ch.6, ADR-005):
 *   1. First request: generate() creates a 64-character hex token and stores
 *      it in $_SESSION['csrf_token'] via Session::set().
 *   2. Every form: csrf_field() helper renders a hidden input with the token.
 *   3. POST requests: validate() compares the submitted token with the
 *      stored token using hash_equals() (timing-safe comparison).
 *   4. After a successful form submission: the caller may call regenerate()
 *      to rotate the token, preventing replay attacks.
 *
 * Integration:
 *   - CsrfMiddleware (future task) calls Csrf::validate() on every POST.
 *   - BaseController::render() (INIT-008) is unaffected — no changes needed.
 *   - csrf_field() helper (INIT-010) calls Csrf::getToken().
 *
 * Requires:
 *   - Session::start() must have been called before any Csrf method.
 *   - Session::isActive() is checked on every call.
 *
 * @package Inveetaire
 * @ref     DBP v1.0 Ch.6, ADR-005, INIT-010
 */
class Csrf
{
    /**
     * Session key used to store the CSRF token.
     * Must match the key read by validate() and the field name in forms.
     */
    const TOKEN_SESSION_KEY = 'csrf_token';

    /**
     * Form field name submitted with POST requests.
     * Must match the name attribute rendered by csrf_field().
     */
    const FORM_FIELD = '_csrf';

    /**
     * Number of random bytes used to generate the token.
     * 32 bytes → 64 hexadecimal characters.
     */
    const TOKEN_BYTES = 32;

    // ─────────────────────────────────────────────────────────────────────────
    // TOKEN GENERATION
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return the current session CSRF token, generating one if absent.
     *
     * Idempotent — calling this multiple times per request returns the
     * same token. A new token is only generated when none exists in the session.
     *
     * @return string  64-character lowercase hexadecimal token.
     */
    public static function getToken(): string
    {
        if (!Session::has(self::TOKEN_SESSION_KEY)) {
            self::generateNew();
        }

        return (string) Session::get(self::TOKEN_SESSION_KEY);
    }

    /**
     * Regenerate the CSRF token, replacing the current one.
     *
     * Call this after a successful form submission to prevent replay attacks.
     * The old token is immediately invalidated — any previous form submission
     * with the old token will fail on the next POST.
     *
     * @return string  The newly generated token.
     */
    public static function regenerate(): string
    {
        return self::generateNew();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // TOKEN VALIDATION
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Validate the CSRF token submitted with the current POST request.
     *
     * Passes non-POST requests without checking (GET, HEAD, OPTIONS are safe).
     *
     * Validation rules:
     *   - Submitted token must not be empty.
     *   - Stored session token must not be empty.
     *   - Comparison uses hash_equals() — timing-safe, prevents timing attacks.
     *
     * @param Request $request  The current HTTP request.
     * @return bool  True if valid (or non-POST); false if token is missing/invalid.
     */
    public static function validate(Request $request): bool
    {
        // Non-POST requests do not require CSRF validation.
        if (!$request->isPost()) {
            return true;
        }

        $submitted = (string) $request->post(self::FORM_FIELD, '');
        $stored    = (string) Session::get(self::TOKEN_SESSION_KEY, '');

        if ($submitted === '' || $stored === '') {
            return false;
        }

        // hash_equals() is timing-safe — prevents timing oracle attacks.
        return hash_equals($stored, $submitted);
    }

    /**
     * Remove the CSRF token from the session.
     *
     * Used when destroying a session (logout). Not for normal invalidation
     * between requests — use regenerate() for that.
     *
     * @return void
     */
    public static function invalidate(): void
    {
        Session::remove(self::TOKEN_SESSION_KEY);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // PRIVATE
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Generate a cryptographically random token and store it in the session.
     *
     * @return string  The newly generated token.
     */
    private static function generateNew(): string
    {
        $token = bin2hex(random_bytes(self::TOKEN_BYTES));
        Session::set(self::TOKEN_SESSION_KEY, $token);

        return $token;
    }
}
