<?php

/**
 * Обмеження швидкості запитів
 *
 * Підтримка різних стратегій обмеження (IP, User, Route)
 *
 * @package Flowaxy\Infrastructure\Security
 * @version 1.0.0 Alpha prerelease
 */

declare(strict_types=1);

namespace Flowaxy\Infrastructure\Security;

use Flowaxy\Contracts\Security\RateLimiterInterface;
use Flowaxy\Infrastructure\Security\RateLimitStrategy;
use Flowaxy\Support\Facades\Log;

final class RateLimiter implements RateLimiterInterface
{
    private RateLimitStrategy $strategy;
    private int $maxRequests;
    private int $windowSeconds;
    private ?object $cache = null;

    /**
     * Конструктор
     *
     * @param RateLimitStrategy $strategy Стратегія обмеження
     * @param int $maxRequests Максимальна кількість запитів
     * @param int $windowSeconds Вікно часу в секундах
     */
    public function __construct(
        RateLimitStrategy $strategy = RateLimitStrategy::IP,
        int $maxRequests = 60,
        int $windowSeconds = 60
    ) {
        $this->strategy = $strategy;
        $this->maxRequests = $maxRequests;
        $this->windowSeconds = $windowSeconds;
    }

    /**
     * Перевірка обмеження швидкості
     *
     * @param string|null $identifier Ідентифікатор (IP, User ID, Route)
     * @return bool True якщо ліміт перевищено
     */
    public function isLimited(?string $identifier = null): bool
    {
        $key = $this->getKey($identifier);
        $cache = $this->getCache();

        if (!$cache) {
            Log::Warning('RateLimiter::isLimited: Cache unavailable, rate limiting disabled', [
                'strategy' => $this->strategy->value,
            ]);
            return false; // Якщо кеш недоступний, не обмежуємо
        }

        $current = $cache->get($key, 0);

        if ($current >= $this->maxRequests) {
            Log::Warning('RateLimiter::isLimited: Rate limit exceeded', [
                'strategy' => $this->strategy->value,
                'identifier' => $identifier,
                'current' => $current,
                'max' => $this->maxRequests,
                'window' => $this->windowSeconds,
            ]);
            return true;
        }

        // Збільшуємо лічильник
        $cache->set($key, $current + 1, $this->windowSeconds);

        Log::Debug('RateLimiter::isLimited: Request allowed', [
            'strategy' => $this->strategy->value,
            'current' => $current + 1,
            'max' => $this->maxRequests,
        ]);

        return false;
    }

    /**
     * Отримання поточного лічильника
     *
     * @param string|null $identifier Ідентифікатор
     * @return int
     */
    public function getCurrentCount(?string $identifier = null): int
    {
        $key = $this->getKey($identifier);
        $cache = $this->getCache();

        if (!$cache) {
            return 0;
        }

        return (int)$cache->get($key, 0);
    }

    /**
     * Отримання залишкового часу до скидання
     *
     * @param string|null $identifier Ідентифікатор
     * @return int Секунди до скидання
     */
    public function getRemainingTime(?string $identifier = null): int
    {
        $key = $this->getKey($identifier);
        $cache = $this->getCache();

        if (!$cache) {
            return 0;
        }

        // Спробуємо отримати TTL з кешу
        // Це спрощена реалізація - в реальності потрібно зберігати час створення
        return $this->windowSeconds;
    }

    /**
     * Скидання ліміту (старий метод для зворотної сумісності)
     *
     * @param string|null $identifier Ідентифікатор
     * @return void
     */
    public function resetLimit(?string $identifier = null): void
    {
        $key = $this->getKey($identifier);
        $cache = $this->getCache();

        if ($cache) {
            $cache->delete($key);
            Log::Info('RateLimiter::reset: Rate limit reset', [
                'strategy' => $this->strategy->value,
                'identifier' => $identifier,
            ]);
        } else {
            Log::Warning('RateLimiter::reset: Cache unavailable, cannot reset rate limit', [
                'strategy' => $this->strategy->value,
            ]);
        }
    }

    /**
     * Скинути лічильник (реалізація інтерфейсу)
     *
     * @param string $key Ключ обмеження
     * @return void
     */
    public function reset(string $key): void
    {
        $cache = $this->getCache();
        if ($cache) {
            $cache->delete($key);
        }
    }

    /**
     * Спробувати виконати дію з обмеженням (реалізація інтерфейсу)
     *
     * @param string $key Ключ обмеження
     * @param int $maxAttempts Максимальна кількість спроб
     * @param callable $callback Функція для виконання
     * @param int $decaySeconds Час життя в секундах
     * @return mixed Результат виконання callback
     */
    public function attempt(string $key, int $maxAttempts, callable $callback, int $decaySeconds = 60): mixed
    {
        if ($this->tooManyAttempts($key, $maxAttempts)) {
            throw new \RuntimeException("Rate limit exceeded for key: {$key}");
        }

        $this->hit($key, $decaySeconds);
        return $callback();
    }

    /**
     * Перевірити, чи занадто багато спроб (реалізація інтерфейсу)
     *
     * @param string $key Ключ обмеження
     * @param int $maxAttempts Максимальна кількість спроб
     * @return bool
     */
    public function tooManyAttempts(string $key, int $maxAttempts): bool
    {
        $current = $this->getCurrent($key);
        return $current >= $maxAttempts;
    }

    /**
     * Збільшити лічильник (реалізація інтерфейсу)
     *
     * @param string $key Ключ обмеження
     * @param int $decaySeconds Час життя в секундах
     * @return int Поточна кількість спроб
     */
    public function hit(string $key, int $decaySeconds = 60): int
    {
        $this->incrementCounter($key, $decaySeconds);
        return $this->getCurrent($key);
    }

    /**
     * Отримати кількість залишкових спроб (реалізація інтерфейсу)
     *
     * @param string $key Ключ обмеження
     * @param int $maxAttempts Максимальна кількість спроб
     * @return int
     */
    public function remaining(string $key, int $maxAttempts): int
    {
        $current = $this->getCurrent($key);
        return max(0, $maxAttempts - $current);
    }

    /**
     * Отримати час до наступної спроби (реалізація інтерфейсу)
     *
     * @param string $key Ключ обмеження
     * @return int Секунд до наступної спроби
     */
    public function availableIn(string $key): int
    {
        $cache = $this->getCache();
        if (!$cache) {
            return 0;
        }
        // Спрощена реалізація - в реальності потрібно зберігати час створення
        return $this->windowSeconds;
    }

    /**
     * Отримати поточну кількість спроб
     *
     * @param string $key Ключ обмеження
     * @return int
     */
    private function getCurrent(string $key): int
    {
        $cache = $this->getCache();
        if (!$cache) {
            return 0;
        }
        return (int)$cache->get($key, 0);
    }

    /**
     * Збільшити лічильник
     *
     * @param string $key Ключ обмеження
     * @param int $decaySeconds Час життя в секундах
     * @return void
     */
    private function incrementCounter(string $key, int $decaySeconds): void
    {
        $cache = $this->getCache();
        if (!$cache) {
            return;
        }
        $current = (int)$cache->get($key, 0);
        $cache->set($key, $current + 1, $decaySeconds);
    }

    /**
     * Генерація ключа для кешу
     *
     * @param string|null $identifier Ідентифікатор
     * @return string
     */
    private function getKey(?string $identifier): string
    {
        $parts = ['rate_limit', $this->strategy->value];

        $additionalParts = match ($this->strategy) {
            RateLimitStrategy::IP => [$identifier ?? $this->getClientIp()],
            RateLimitStrategy::User => [($identifier ?? $this->getUserId()) ?: $this->getClientIp()],
            RateLimitStrategy::Route => [$identifier ?? $this->getCurrentRoute()],
            RateLimitStrategy::IPAndRoute => [$this->getClientIp(), $identifier ?? $this->getCurrentRoute()],
        };

        $parts = array_merge($parts, $additionalParts);

        return implode(':', $parts);
    }

    /**
     * Отримання IP адреси клієнта
     */
    private function getClientIp(): string
    {
        if (class_exists(Security::class)) {
            return Security::getClientIp();
        }

        return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    }

    /**
     * Отримання ID користувача
     */
    private function getUserId(): ?string
    {
        if (function_exists('sessionManager')) {
            $session = sessionManager();
            return $session->get('user_id');
        }

        return null;
    }

    /**
     * Отримання поточного маршруту
     */
    private function getCurrentRoute(): string
    {
        $uri = $_SERVER['REQUEST_URI'] ?? '/';
        $path = parse_url($uri, PHP_URL_PATH);
        return $path ?? '/';
    }

    /**
     * Отримання кешу
     */
    private function getCache(): ?object
    {
        if ($this->cache !== null) {
            return $this->cache;
        }

        if (function_exists('cache')) {
            $this->cache = cache();
            return $this->cache;
        }

        return null;
    }
}
