<?php
require_once __DIR__ . '/db.php';

/**
 * Per-IP rate limit. DB-backed counter per (purpose, IP, window).
 *
 * Returns true if the request is allowed. Returns false if the
 * limit has been hit — caller should respond with 429 or similar.
 *
 * @param string $purpose  'login' | 'signup' | 'forgot' | 'itn' | ...
 * @param int    $limit    max hits per window
 * @param int    $window_minutes  window duration
 */
function rate_limit_check(string $purpose, int $limit, int $window_minutes = null): bool {
    if ($window_minutes === null) $window_minutes = RL_WINDOW_MINUTES;
    if ($limit <= 0) return true;

    $ip = $_SERVER['REMOTE_ADDR'] ?? 'cli';
    $key_hash = hash('sha256', $purpose . '|' . $ip);

    // Window bucket: snap "now" down to the current window boundary
    $now = time();
    $bucket_seconds = $window_minutes * 60;
    $bucket_start = $now - ($now % $bucket_seconds);
    $window_key = $purpose . ':' . date('Y-m-d H:i', $bucket_start);

    try {
        // Upsert the counter
        db_exec(
            'INSERT INTO rate_limits (key_hash, window_key, hits)
                VALUES (:k, :w, 1)
                ON DUPLICATE KEY UPDATE hits = hits + 1',
            ['k' => $key_hash, 'w' => $window_key]
        );

        $hits = (int)db_value(
            'SELECT hits FROM rate_limits WHERE key_hash = :k AND window_key = :w',
            ['k' => $key_hash, 'w' => $window_key]
        );

        return $hits <= $limit;
    } catch (Throwable $e) {
        // Never block legitimate users because the limiter itself broke
        error_log('rate_limit_check failed: ' . $e->getMessage());
        return true;
    }
}

/**
 * Enforce — returns 429 and exits if over the limit.
 * Convenience wrapper for POST handlers.
 */
function rate_limit_enforce(string $purpose, int $limit, int $window_minutes = null): void {
    if (rate_limit_check($purpose, $limit, $window_minutes)) return;

    http_response_code(429);
    header('Retry-After: ' . (($window_minutes ?? RL_WINDOW_MINUTES) * 60));
    echo 'Too many requests. Please wait a few minutes and try again.';
    exit;
}

/**
 * Clean up old rate-limit buckets. Call from a daily cron to keep
 * the table small. Idempotent — safe to run on every cron pass.
 */
function rate_limit_cleanup(): int {
    try {
        return db_exec('DELETE FROM rate_limits WHERE updated_at < DATE_SUB(NOW(), INTERVAL 1 DAY)');
    } catch (Throwable $e) {
        return 0;
    }
}
