<?php
/**
 * auth_helpers.php — CSRF tokens, password hashing, and login-attempt
 * lockout. Pure functions only (no bootstrapping/side effects) — see
 * includes/session.php for the "is this request logged in" logic that
 * builds on top of these.
 */

// =============================================================================
// CSRF Tokens
// =============================================================================

function generate_token(): string {
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function verify_token(string $token): bool {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

// =============================================================================
// Password Security Functions
// =============================================================================

/**
 * Hash a password using bcrypt with configurable cost.
 */
function hash_password(string $password): string {
    global $bcryptCost;
    return password_hash($password, PASSWORD_BCRYPT, ['cost' => $bcryptCost]);
}

/**
 * Verify a password against a stored hash.
 * Also handles legacy plaintext passwords by upgrading them on successful match.
 */
function verify_password(string $password, string $storedHash): bool {
    // Check if the stored value is a bcrypt hash
    if (str_starts_with($storedHash, '$2y$') || str_starts_with($storedHash, '$2b$')) {
        return password_verify($password, $storedHash);
    }

    // Legacy plaintext comparison — if it matches, return true
    // The caller should upgrade the hash after successful login
    return hash_equals($storedHash, $password);
}

/**
 * Check if a password hash needs to be rehashed (cost changed).
 */
function password_needs_rehash_check(string $hash): bool {
    global $bcryptCost;
    return password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => $bcryptCost]);
}

/**
 * Check if an account is locked due to failed attempts.
 * Returns true if locked, false if unlocked.
 */
function is_account_locked(array $userData): bool {
    if (empty($userData['locked_until'])) {
        return false;
    }
    return strtotime($userData['locked_until']) > time();
}

/**
 * Record a failed login attempt. Lock account after 5 failures.
 */
function record_failed_login(object $db, int $userId, int $currentAttempts): void {
    $attempts = $currentAttempts + 1;
    $lockUntil = null;

    // Lock for 15 minutes after 5 failed attempts
    if ($attempts >= 5) {
        $lockUntil = date('Y-m-d H:i:s', time() + 900);
    }

    $db->update('user', [
        'failed_attempts' => $attempts,
        'locked_until' => $lockUntil,
    ], 'id = :id', [':id' => $userId]);
}

/**
 * Reset failed login attempts on successful login.
 */
function reset_failed_login(object $db, int $userId): void {
    $db->update('user', [
        'failed_attempts' => 0,
        'locked_until' => null,
    ], 'id = :id', [':id' => $userId]);
}

// =============================================================================
// Per-IP login throttle
// =============================================================================
// Complements the per-account lockout above. The account lockout only
// triggers when a KNOWN username is attacked; a per-IP throttle also slows
// down attempts against unknown usernames and distributed brute-force.
// Uses a small file-based counter (no DB schema change needed). Falls back
// to a session counter if the file can't be written.

function login_throttle_dir(): string {
    $dir = __DIR__ . '/../panel-data/login-throttle';
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }
    return $dir;
}

function client_ip(): string {
    // Respect a trusted proxy (e.g. Cloudflare) if present, else REMOTE_ADDR.
    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
        $ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        $ip = trim($parts[0]);
    }
    return preg_replace('/[^0-9a-fA-F:.]/', '', $ip) ?: '0.0.0.0';
}

/**
 * Returns the number of failed login attempts recorded for this IP in the
 * current window, or null if the throttle is unavailable (treat as 0).
 */
function login_throttle_count(): ?int {
    $ip = client_ip();
    $file = login_throttle_dir() . '/' . md5($ip) . '.json';
    if (is_file($file)) {
        $data = @json_decode((string) @file_get_contents($file), true);
        if (is_array($data) && isset($data['count'], $data['window_start'])) {
            // Reset the window if it has expired (10 minutes).
            if (time() - (int) $data['window_start'] > 600) {
                return 0;
            }
            return (int) $data['count'];
        }
    }
    return 0;
}

/**
 * Records a failed login attempt for this IP. Returns the new count.
 */
function login_throttle_record(): int {
    $ip = client_ip();
    $dir = login_throttle_dir();
    $file = $dir . '/' . md5($ip) . '.json';
    $count = 1;
    $windowStart = time();
    if (is_file($file)) {
        $data = @json_decode((string) @file_get_contents($file), true);
        if (is_array($data) && isset($data['count'], $data['window_start'])) {
            if (time() - (int) $data['window_start'] <= 600) {
                $count = (int) $data['count'] + 1;
                $windowStart = (int) $data['window_start'];
            }
        }
    }
    @file_put_contents($file, json_encode(['count' => $count, 'window_start' => $windowStart]), LOCK_EX);
    return $count;
}

/**
 * Clears the failed-attempt counter for this IP (on successful login).
 */
function login_throttle_clear(): void {
    $file = login_throttle_dir() . '/' . md5(client_ip()) . '.json';
    if (is_file($file)) {
        @unlink($file);
    }
}

/**
 * Returns true if this IP has exceeded the allowed failed attempts in the
 * window (default: 10 per 10 minutes).
 */
function login_throttle_blocked(int $max = 10): bool {
    return login_throttle_count() >= $max;
}
