<?php

declare(strict_types=1);

class RateLimiter
{
    

    public static function throttle(string $action, int $maxPerHour = 60, int $maxPerDay = 300): bool
    {
        $ip = self::ip();
        $fp = self::fingerprint();

        $allowed = self::attempt("{$action}:h:{$ip}", $maxPerHour, 3600)
                && self::attempt("{$action}:d:{$ip}", $maxPerDay, 86400);

        if ($fp) {
            $allowed = self::attempt("{$action}:h:{$fp}", $maxPerHour, 3600) && $allowed;
            $allowed = self::attempt("{$action}:d:{$fp}", $maxPerDay, 86400) && $allowed;
        }

        return $allowed;
    }

    

    public static function attempt(string $key, int $max, int $windowSec): bool
    {
        Database::query(
            'INSERT INTO rate_limits (action_key, attempts, window_start) VALUES (?, 1, NOW())
             ON DUPLICATE KEY UPDATE
                attempts = IF(TIMESTAMPDIFF(SECOND, window_start, NOW()) >= ?, 1, attempts + 1),
                window_start = IF(TIMESTAMPDIFF(SECOND, window_start, NOW()) >= ?, NOW(), window_start)',
            [$key, $windowSec, $windowSec]
        );

        $row = Database::fetchOne('SELECT attempts FROM rate_limits WHERE action_key = ?', [$key]);

        self::maybeCleanup();

        return $row && (int)$row['attempts'] <= $max;
    }

    

    public static function fingerprint(): string
    {
        if (!empty($_COOKIE['_tp_fp'])) {
            $fp = $_COOKIE['_tp_fp'];
            if (preg_match('/^[a-f0-9]{64}$/', $fp)) {
                return $fp;
            }
        }

        $fp = bin2hex(random_bytes(32));

        setcookie('_tp_fp', $fp, [
            'expires' => time() + 365 * 86400,
            'path' => '/',
             
             
             
            'secure' => !empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off',
            'httponly' => true,
            'samesite' => 'Lax',
        ]);

        $_COOKIE['_tp_fp'] = $fp;

        return $fp;
    }

    public static function ip(): string
    {
         
         
         
         
         
        $ip = trim($_SERVER['REMOTE_ADDR'] ?? '');

        return filter_var($ip, FILTER_VALIDATE_IP) ?: '0.0.0.0';
    }

    private static function maybeCleanup(): void
    {
        if (random_int(1, 100) !== 1) {
            return;
        }
        Database::query('DELETE FROM rate_limits WHERE window_start < DATE_SUB(NOW(), INTERVAL 48 HOUR)');
    }
}
