<?php

require_once __DIR__ . '/settings.php';

function qdb_login_rate_limit_path(): string
{
    return dirname(QDB_PATH) . '/.login_rate_limit.json';
}

function qdb_client_ip(): string
{
    $raw = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
    if ($raw === '') {
        return '0.0.0.0';
    }
    if (str_contains($raw, ',')) {
        $raw = trim(explode(',', $raw)[0]);
    }
    return $raw;
}

function qdb_login_rate_limit_key(string $username): string
{
    $ip = qdb_client_ip();
    return hash('sha256', strtolower(trim($username)) . "\0" . $ip);
}

/**
 * @return array{0: bool, 1: int} allowed, retry_after_seconds
 */
function qdb_login_rate_limit_check(string $username, ?array $settings = null): array
{
    $settings ??= qdb_settings_get();
    $max = (int)$settings['login_max_attempts'];
    $window = (int)$settings['login_window_seconds'];
    $lockout = (int)$settings['login_lockout_seconds'];
    $now = time();
    $key = qdb_login_rate_limit_key($username);
    $path = qdb_login_rate_limit_path();
    $dir = dirname($path);
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }

    $fp = @fopen($path, 'c+');
    if ($fp === false) {
        return [true, 0];
    }
    try {
        if (!flock($fp, LOCK_EX)) {
            return [true, 0];
        }
        $raw = stream_get_contents($fp);
        $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : [];
        if (!is_array($data)) {
            $data = [];
        }
        $entry = $data[$key] ?? ['attempts' => [], 'locked_until' => 0];
        $lockedUntil = (int)($entry['locked_until'] ?? 0);
        if ($lockedUntil > $now) {
            return [false, $lockedUntil - $now];
        }
        $attempts = array_values(array_filter(
            (array)($entry['attempts'] ?? []),
            static fn($t) => is_int($t) && $t > $now - $window
        ));
        if (count($attempts) >= $max) {
            $entry['locked_until'] = $now + $lockout;
            $entry['attempts'] = $attempts;
            $data[$key] = $entry;
            ftruncate($fp, 0);
            rewind($fp);
            fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE));
            fflush($fp);
            return [false, $lockout];
        }
        return [true, 0];
    } finally {
        flock($fp, LOCK_UN);
        fclose($fp);
    }
}

function qdb_login_rate_limit_record_failure(string $username, ?array $settings = null): int
{
    $settings ??= qdb_settings_get();
    $max = (int)$settings['login_max_attempts'];
    $window = (int)$settings['login_window_seconds'];
    $lockout = (int)$settings['login_lockout_seconds'];
    $now = time();
    $key = qdb_login_rate_limit_key($username);
    $path = qdb_login_rate_limit_path();
    $dir = dirname($path);
    if (!is_dir($dir)) {
        @mkdir($dir, 0755, true);
    }

    $fp = @fopen($path, 'c+');
    if ($fp === false) {
        return 0;
    }
    try {
        if (!flock($fp, LOCK_EX)) {
            return 0;
        }
        $raw = stream_get_contents($fp);
        $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : [];
        if (!is_array($data)) {
            $data = [];
        }
        $entry = $data[$key] ?? ['attempts' => [], 'locked_until' => 0];
        $attempts = array_values(array_filter(
            (array)($entry['attempts'] ?? []),
            static fn($t) => is_int($t) && $t > $now - $window
        ));
        $attempts[] = $now;
        $retry = 0;
        if (count($attempts) >= $max) {
            $entry['locked_until'] = $now + $lockout;
            $retry = $lockout;
        }
        $entry['attempts'] = $attempts;
        $data[$key] = $entry;
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE));
        fflush($fp);
        return $retry;
    } finally {
        flock($fp, LOCK_UN);
        fclose($fp);
    }
}

function qdb_login_rate_limit_clear(string $username): void
{
    $key = qdb_login_rate_limit_key($username);
    $path = qdb_login_rate_limit_path();
    if (!is_file($path)) {
        return;
    }
    $fp = @fopen($path, 'c+');
    if ($fp === false) {
        return;
    }
    try {
        if (!flock($fp, LOCK_EX)) {
            return;
        }
        $raw = stream_get_contents($fp);
        $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : [];
        if (!is_array($data) || !isset($data[$key])) {
            return;
        }
        unset($data[$key]);
        ftruncate($fp, 0);
        rewind($fp);
        fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE));
        fflush($fp);
    } finally {
        flock($fp, LOCK_UN);
        fclose($fp);
    }
}

function qdb_login_rate_limit_deny(int $retryAfterSeconds): never
{
    if ($retryAfterSeconds > 0) {
        header('Retry-After: ' . (string)$retryAfterSeconds);
    }
    json_error(
        'RATE_LIMITED',
        'Too many login attempts. Please wait before trying again.',
        429
    );
}
