<?php

declare(strict_types=1);

namespace SeoScope\Services;

/**
 * Conservative sliding-window rate limiting for dashboard SEO AI LLM requests.
 *
 * Two independent limits (both optional via config): per logged-in user (session)
 * and per client IP (typical single-server install: JSON file under storage + flock).
 */
final class SeoAiAssistantRateLimiter
{
    private const SESSION_KEY_PREFIX = '_ss_seo_ai_llm_rl_';

    public function __construct(
        private array $config,
        private string $rateLimitStorageDir,
        private string $secretPepper,
    ) {}

    /**
     * @return array{
     *   session_enabled: bool,
     *   session_window: int,
     *   session_max: int,
     *   ip_enabled: bool,
     *   ip_window: int,
     *   ip_max: int,
     * }
     */
    public static function resolvedLimits(array $config): array
    {
        $rl = isset($config['seo_ai']['rate_limit']) && is_array($config['seo_ai']['rate_limit'])
            ? $config['seo_ai']['rate_limit']
            : [];
        $sess = isset($rl['per_user_session']) && is_array($rl['per_user_session'])
            ? $rl['per_user_session']
            : [];
        $ip = isset($rl['per_ip']) && is_array($rl['per_ip'])
            ? $rl['per_ip']
            : [];

        return [
            'session_enabled' => self::truthy($sess['enabled'] ?? true),
            'session_window' => max(60, (int) ($sess['window_seconds'] ?? 3600)),
            'session_max' => max(1, (int) ($sess['max_requests'] ?? 24)),
            'ip_enabled' => self::truthy($ip['enabled'] ?? true),
            'ip_window' => max(60, (int) ($ip['window_seconds'] ?? 3600)),
            'ip_max' => max(1, (int) ($ip['max_requests'] ?? 72)),
        ];
    }

    /**
     * @return bool true if the request may proceed (slot consumed)
     */
    public function tryConsume(int $uid, ?string $clientIp): bool
    {
        $lim = self::resolvedLimits($this->config);
        $now = time();

        if ($lim['session_enabled'] && !$this->consumeSessionWindow($uid, $lim['session_window'], $lim['session_max'], $now)) {
            return false;
        }

        if ($lim['ip_enabled'] && $clientIp !== null && $clientIp !== '') {
            if (!$this->consumeIpWindow($clientIp, $lim['ip_window'], $lim['ip_max'], $now)) {
                return false;
            }
        }

        return true;
    }

    private function consumeSessionWindow(int $uid, int $window, int $max, int $now): bool
    {
        if ($uid < 1) {
            return false;
        }

        $key = self::SESSION_KEY_PREFIX . $uid;
        $list = $_SESSION[$key] ?? [];
        if (!is_array($list)) {
            $list = [];
        }
        /** @var list<int> $pruned */
        $pruned = array_values(array_filter($list, static fn ($t) => is_int($t) && ($now - $t) < $window));
        if (count($pruned) >= $max) {
            return false;
        }
        $pruned[] = $now;
        $_SESSION[$key] = $pruned;

        return true;
    }

    private function consumeIpWindow(string $ip, int $window, int $max, int $now): bool
    {
        $dir = rtrim($this->rateLimitStorageDir, '/');
        if ($dir !== '' && !is_dir($dir)) {
            @mkdir($dir, 0775, true);
        }
        $hash = substr(hash('sha256', $ip . "\0" . $this->secretPepper), 0, 48);
        $path = $dir . '/seo_ai_llm_ip_' . $hash . '.json';

        $fh = @fopen($path, 'c+');
        if ($fh === false) {
            return true;
        }

        try {
            if (!flock($fh, LOCK_EX)) {
                return true;
            }
            rewind($fh);
            $raw = stream_get_contents($fh);
            $list = [];
            if (is_string($raw) && $raw !== '') {
                $decoded = json_decode($raw, true);
                if (is_array($decoded) && isset($decoded['t']) && is_array($decoded['t'])) {
                    foreach ($decoded['t'] as $item) {
                        if (is_int($item)) {
                            $list[] = $item;
                        }
                    }
                }
            }
            /** @var list<int> $pruned */
            $pruned = array_values(array_filter($list, static fn ($t) => ($now - $t) < $window));
            if (count($pruned) >= $max) {
                flock($fh, LOCK_UN);

                return false;
            }
            $pruned[] = $now;

            $payload = json_encode(['t' => $pruned], JSON_UNESCAPED_SLASHES);
            if (!is_string($payload)) {
                flock($fh, LOCK_UN);

                return true;
            }
            ftruncate($fh, 0);
            rewind($fh);
            fwrite($fh, $payload);
            fflush($fh);
            flock($fh, LOCK_UN);
        } finally {
            fclose($fh);
        }

        return true;
    }

    private static function truthy(mixed $value): bool
    {
        if ($value === true) {
            return true;
        }
        if ($value === false || $value === null) {
            return false;
        }
        $s = strtolower(trim((string) $value));

        return $s === '1' || $s === 'true' || $s === 'yes' || $s === 'on';
    }
}
