<?php

declare(strict_types=1);

namespace ApiModel\Infrastructure\Security;

use ApiModel\Shared\Http\ErrorResponse;
use Predis\Client as RedisClient;
use Throwable;

/**
 * Limitación por IP: Redis si está configurado (REDIS_HOST o REDIS_URL), si no archivo JSON local.
 * Si Redis falla en tiempo de ejecución, se hace fallback al archivo.
 */
final class IpRateThrottle
{
    private static ?RedisClient $redis = null;

    public static function enforce(string $bucket, int $max, int $windowSeconds): void
    {
        $keySuffix = self::keySuffix($bucket);
        if (self::redisEnabled()) {
            try {
                self::enforceRedis($keySuffix, $max, $windowSeconds);

                return;
            } catch (Throwable $e) {
                error_log('[api] Redis rate limit no disponible, usando archivo: ' . $e->getMessage());
            }
        }

        self::enforceFile($keySuffix, $max, $windowSeconds);
    }

    private static function keySuffix(string $bucket): string
    {
        $ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
        if ($ip === '') {
            $ip = 'unknown';
        }

        $safeIp = preg_replace('/[^a-zA-Z0-9.:_-]/', '_', $ip);
        if ($safeIp === null) {
            $safeIp = 'unknown';
        }

        return $bucket . ':' . $safeIp;
    }

    private static function redisEnabled(): bool
    {
        $url = trim((string) ($_ENV['REDIS_URL'] ?? ''));
        $host = trim((string) ($_ENV['REDIS_HOST'] ?? ''));

        return $url !== '' || $host !== '';
    }

    private static function redisPrefix(): string
    {
        $p = trim((string) ($_ENV['REDIS_KEY_PREFIX'] ?? 'app:'));

        return rtrim($p, ':') . ':rl:';
    }

    /**
     * Índice de base Redis: igual que en api_modelo (`REDIS_DATABASE`), con alias `REDIS_DB`.
     * Por defecto 1 para aislar rate limit del resto de servicios que suelen usar 0.
     */
    private static function redisDatabaseIndex(): int
    {
        $raw = $_ENV['REDIS_DATABASE'] ?? $_ENV['REDIS_DB'] ?? '1';

        return (int) $raw;
    }

    /** @return array<string, float|int|string> */
    private static function redisClientOptions(): array
    {
        $options = [];
        $timeout = trim((string) ($_ENV['REDIS_TIMEOUT'] ?? ''));
        if ($timeout !== '' && is_numeric($timeout)) {
            $options['read_write_timeout'] = (float) $timeout;
        }

        return $options;
    }

    private static function redisClient(): RedisClient
    {
        if (self::$redis instanceof RedisClient) {
            return self::$redis;
        }

        $options = self::redisClientOptions();

        $url = trim((string) ($_ENV['REDIS_URL'] ?? ''));
        if ($url !== '') {
            self::$redis = new RedisClient($url, $options);
        } else {
            $host = trim((string) ($_ENV['REDIS_HOST'] ?? '127.0.0.1'));
            $port = (int) ($_ENV['REDIS_PORT'] ?? 6379);
            $db = self::redisDatabaseIndex();
            $pass = trim((string) ($_ENV['REDIS_PASSWORD'] ?? ''));
            $params = [
                'scheme' => 'tcp',
                'host' => $host,
                'port' => $port,
                'database' => $db,
            ];
            if ($pass !== '') {
                $params['password'] = $pass;
            }
            self::$redis = new RedisClient($params, $options);
        }

        return self::$redis;
    }

    private static function enforceRedis(string $keySuffix, int $max, int $windowSeconds): void
    {
        $fullKey = self::redisPrefix() . $keySuffix;
        $client = self::redisClient();
        $n = (int) $client->incr($fullKey);
        if ($n === 1 || (int) $client->ttl($fullKey) < 0) {
            $client->expire($fullKey, max(1, $windowSeconds));
        }

        if ($n > $max) {
            $ttl = (int) $client->ttl($fullKey);
            self::deny429(max(1, $ttl > 0 ? $ttl : $windowSeconds));
        }
    }

    private static function enforceFile(string $keySuffix, int $max, int $windowSeconds): void
    {
        $key = $keySuffix;
        $store = self::readStore();
        $now = time();
        $entry = $store[$key] ?? ['count' => 0, 'reset_at' => $now + $windowSeconds];

        if ($entry['reset_at'] <= $now) {
            $entry = ['count' => 0, 'reset_at' => $now + $windowSeconds];
        }

        $entry['count']++;
        $store[$key] = $entry;
        self::writeStore($store);

        if ($entry['count'] > $max) {
            $retryAfter = max(1, (int) $entry['reset_at'] - $now);
            self::deny429($retryAfter);
        }
    }

    private static function storePath(): string
    {
        return dirname(__DIR__, 3) . '/storage/rate-limit.json';
    }

    /** @return array<string, array{count: int, reset_at: int}> */
    private static function readStore(): array
    {
        $path = self::storePath();
        $dir = dirname($path);
        if (! is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }

        if (! is_file($path)) {
            return [];
        }

        $raw = @file_get_contents($path);
        if (! is_string($raw) || $raw === '') {
            return [];
        }

        $decoded = json_decode($raw, true);
        if (! is_array($decoded)) {
            return [];
        }

        return $decoded;
    }

    /** @param array<string, array{count: int, reset_at: int}> $store */
    private static function writeStore(array $store): void
    {
        $path = self::storePath();
        @file_put_contents($path, json_encode($store, JSON_UNESCAPED_UNICODE));
    }

    private static function deny429(int $retryAfter): never
    {
        header('Retry-After: ' . (string) $retryAfter);
        http_response_code(429);
        header('Content-Type: application/json; charset=utf-8');
        $payload = ErrorResponse::payload(
            429,
            'Demasiadas solicitudes. Intenta de nuevo en unos segundos.',
            ErrorResponse::defaultCodeForStatus(429),
        );
        echo json_encode($payload, JSON_UNESCAPED_UNICODE);
        exit;
    }
}
