<?php
/**
 * includes/security.php — Supernova 2.0 security core
 *
 * Loaded automatically by config.php on every request. Provides:
 *   - HTTP security headers        send_security_headers()
 *   - CSRF tokens                  csrf_token(), csrf_field(), verify_csrf(), require_csrf()
 *   - Login brute-force throttle   throttle_check(), throttle_record(), throttle_clear()
 */

// ─────────────────────────────────────────────────────────────────────────────
// Security headers (safe on every response type)
// ─────────────────────────────────────────────────────────────────────────────
function send_security_headers(): void {
    if (headers_sent()) return;
    header('X-Content-Type-Options: nosniff');
    header('X-Frame-Options: SAMEORIGIN');
    header('Referrer-Policy: strict-origin-when-cross-origin');
    header('Permissions-Policy: camera=(self), microphone=(), geolocation=(), payment=(self)');
    header_remove('X-Powered-By');
}

// ─────────────────────────────────────────────────────────────────────────────
// CSRF protection
// ─────────────────────────────────────────────────────────────────────────────

/** Get (or create) the per-session CSRF token. */
function csrf_token(): string {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/** Hidden input for classic <form method="POST"> submissions. */
function csrf_field(): string {
    return '<input type="hidden" name="csrf_token" value="' .
           htmlspecialchars(csrf_token(), ENT_QUOTES) . '">';
}

/**
 * Validate a submitted token. Checks, in order:
 *   1. POST field  csrf_token
 *   2. Header      X-CSRF-Token   (for fetch/AJAX)
 *   3. JSON body   csrf_token     (optional pre-decoded payload)
 */
function verify_csrf(?array $jsonBody = null): bool {
    $sent = $_POST['csrf_token']
        ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? null)
        ?? ($jsonBody['csrf_token'] ?? null);
    if (!is_string($sent) || $sent === '') return false;
    return hash_equals(csrf_token(), $sent);
}

/**
 * Enforce CSRF on state-changing requests; responds 403 and exits on failure.
 * $asJson controls the error body format (true for API endpoints).
 */
function require_csrf(bool $asJson = false, ?array $jsonBody = null): void {
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
    if (verify_csrf($jsonBody)) return;

    http_response_code(403);
    if ($asJson) {
        header('Content-Type: application/json');
        echo json_encode(['error' => 'Invalid or missing security token. Please refresh the page and try again.']);
    } else {
        echo 'Invalid or missing security token (CSRF). Please go back, refresh the page, and try again.';
    }
    exit;
}

// ─────────────────────────────────────────────────────────────────────────────
// Login throttling — sliding window per email+IP
// ─────────────────────────────────────────────────────────────────────────────

const THROTTLE_MAX_ATTEMPTS = 5;    // allowed failures…
const THROTTLE_WINDOW_MIN   = 10;   // …per this many minutes

function throttle_identifier(string $email): string {
    $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    return strtolower(trim($email)) . '|' . $ip;
}

/** True while the identifier is locked out. */
function throttle_check(PDO $pdo, string $email): bool {
    try {
        $stmt = $pdo->prepare(
            "SELECT COUNT(*) FROM login_throttle
             WHERE identifier = ? AND attemptedAt > (NOW() - INTERVAL " . THROTTLE_WINDOW_MIN . " MINUTE)"
        );
        $stmt->execute([throttle_identifier($email)]);
        return $stmt->fetchColumn() >= THROTTLE_MAX_ATTEMPTS;
    } catch (PDOException $e) {
        // Auto-heal: create table if missing
        if ($e->getCode() === '42S02') {
            $pdo->exec("CREATE TABLE IF NOT EXISTS `login_throttle` (`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, `identifier` varchar(320) NOT NULL, `attemptedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `ident_time` (`identifier`, `attemptedAt`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
        }
        return false;
    }
}

/** Record a failed attempt (and opportunistically prune old rows). */
function throttle_record(PDO $pdo, string $email): void {
    try {
        $stmt = $pdo->prepare("INSERT INTO login_throttle (identifier) VALUES (?)");
        $stmt->execute([throttle_identifier($email)]);
        if (random_int(1, 20) === 1) { // occasional cleanup
            $pdo->query("DELETE FROM login_throttle WHERE attemptedAt < (NOW() - INTERVAL 1 DAY)");
        }
    } catch (PDOException $e) {
        // Silently catch if table missing or creation fails
    }
}

/** Clear attempts after a successful login. */
function throttle_clear(PDO $pdo, string $email): void {
    $stmt = $pdo->prepare("DELETE FROM login_throttle WHERE identifier = ?");
    $stmt->execute([throttle_identifier($email)]);
}
