<?php
/**
 * NIS2 Agile - Rate Limit Service
 *
 * Rate limiting basato su file per proteggere gli endpoint API da abusi.
 * Supporta finestre multiple per endpoint (es. 5/min + 20/ora).
 * Storage: /tmp/nis2_ratelimit/{md5(key)}.json
 */

class RateLimitService
{
    private const STORAGE_DIR = '/tmp/nis2_ratelimit';

    /**
     * Verifica il rate limit e lancia eccezione se superato
     *
     * @param string $key   Identificatore univoco (es. "login:192.168.1.1", "ai:user_42")
     * @param array  $limits Array di finestre [['max' => int, 'window_seconds' => int], ...]
     * @throws RuntimeException Se il rate limit è superato
     */
    public static function check(string $key, array $limits): void
    {
        $timestamps = self::loadTimestamps($key);
        $now = time();

        foreach ($limits as $limit) {
            $windowStart = $now - $limit['window_seconds'];
            $hitsInWindow = count(array_filter($timestamps, fn(int $ts): bool => $ts > $windowStart));

            if ($hitsInWindow >= $limit['max']) {
                // Calcola secondi rimanenti fino alla scadenza del hit più vecchio nella finestra
                $oldestInWindow = array_values(array_filter($timestamps, fn(int $ts): bool => $ts > $windowStart));
                sort($oldestInWindow);
                $retryAfter = ($oldestInWindow[0] ?? $now) + $limit['window_seconds'] - $now;
                $retryAfter = max(1, $retryAfter);

                throw new RuntimeException(
                    "Troppi tentativi. Riprova tra {$retryAfter} secondi.",
                    429
                );
            }
        }
    }

    /**
     * Registra un hit per la chiave specificata
     *
     * @param string $key Identificatore univoco
     */
    public static function increment(string $key): void
    {
        $timestamps = self::loadTimestamps($key);
        $timestamps[] = time();

        self::saveTimestamps($key, $timestamps);
    }

    /**
     * Verifica se il rate limit è superato senza lanciare eccezione
     *
     * @param string $key    Identificatore univoco
     * @param array  $limits Array di finestre
     * @return bool True se il limite è superato
     */
    public static function isExceeded(string $key, array $limits): bool
    {
        try {
            self::check($key, $limits);
            return false;
        } catch (RuntimeException) {
            return true;
        }
    }

    /**
     * Restituisce i tentativi rimanenti per ogni finestra
     *
     * @param string $key    Identificatore univoco
     * @param array  $limits Array di finestre
     * @return array Array di ['max' => int, 'window_seconds' => int, 'remaining' => int, 'reset_at' => int]
     */
    public static function getRemaining(string $key, array $limits): array
    {
        $timestamps = self::loadTimestamps($key);
        $now = time();
        $result = [];

        foreach ($limits as $limit) {
            $windowStart = $now - $limit['window_seconds'];
            $hitsInWindow = array_filter($timestamps, fn(int $ts): bool => $ts > $windowStart);
            $hitCount = count($hitsInWindow);

            // Calcola quando si resetta la finestra (scadenza del hit più vecchio)
            $resetAt = $now + $limit['window_seconds'];
            if (!empty($hitsInWindow)) {
                $oldest = min($hitsInWindow);
                $resetAt = $oldest + $limit['window_seconds'];
            }

            $result[] = [
                'max'            => $limit['max'],
                'window_seconds' => $limit['window_seconds'],
                'remaining'      => max(0, $limit['max'] - $hitCount),
                'reset_at'       => $resetAt,
            ];
        }

        return $result;
    }

    /**
     * Pulisce i file di rate limit scaduti
     * Da chiamare periodicamente (es. cron ogni 10 minuti)
     */
    public static function cleanup(): void
    {
        $dir = self::STORAGE_DIR;

        if (!is_dir($dir)) {
            return;
        }

        $now = time();
        // Finestra massima ragionevole: 1 ora. Elimina file non modificati da più di 2 ore
        $maxAge = 7200;

        $files = glob($dir . '/*.json');
        if ($files === false) {
            return;
        }

        foreach ($files as $file) {
            $mtime = filemtime($file);
            if ($mtime !== false && ($now - $mtime) > $maxAge) {
                @unlink($file);
                continue;
            }

            // Pulisci anche i timestamp scaduti all'interno dei file ancora attivi
            $content = @file_get_contents($file);
            if ($content === false) {
                continue;
            }

            $timestamps = json_decode($content, true);
            if (!is_array($timestamps)) {
                @unlink($file);
                continue;
            }

            $cutoff = $now - $maxAge;
            $filtered = array_values(array_filter($timestamps, fn(int $ts): bool => $ts > $cutoff));

            if (empty($filtered)) {
                @unlink($file);
            } else {
                @file_put_contents($file, json_encode($filtered), LOCK_EX);
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Metodi privati
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Carica i timestamp dal file di storage
     */
    private static function loadTimestamps(string $key): array
    {
        $file = self::getFilePath($key);

        if (!file_exists($file)) {
            return [];
        }

        $content = @file_get_contents($file);
        if ($content === false) {
            return [];
        }

        $timestamps = json_decode($content, true);

        return is_array($timestamps) ? $timestamps : [];
    }

    /**
     * Salva i timestamp nel file di storage
     */
    private static function saveTimestamps(string $key, array $timestamps): void
    {
        $dir = self::STORAGE_DIR;

        if (!is_dir($dir)) {
            if (!mkdir($dir, 0700, true) && !is_dir($dir)) {
                throw new RuntimeException("Impossibile creare directory rate limit: {$dir}");
            }
        }

        $file = self::getFilePath($key);

        if (file_put_contents($file, json_encode($timestamps), LOCK_EX) === false) {
            throw new RuntimeException("Impossibile scrivere file rate limit: {$file}");
        }
    }

    /**
     * Genera il percorso del file per una chiave
     */
    private static function getFilePath(string $key): string
    {
        return self::STORAGE_DIR . '/' . md5($key) . '.json';
    }
}
