<?php

/**
 * Simple file-based rate limiter
 * Prevents abuse by limiting requests per IP address
 */
class RateLimit {
    private $cacheDir;
    private $maxRequests;
    private $timeWindow;

    /**
     * Constructor
     * @param int $maxRequests Maximum requests allowed in time window
     * @param int $timeWindow Time window in seconds
     * @param string $cacheDir Directory to store rate limit data
     */
    public function __construct($maxRequests = 30, $timeWindow = 60, $cacheDir = null) {
        $this->maxRequests = $maxRequests;
        $this->timeWindow = $timeWindow;
        $this->cacheDir = $cacheDir ?? dirname(__DIR__) . '/storage/cache/rate_limit';

        // Create cache directory if it doesn't exist
        if (!is_dir($this->cacheDir)) {
            @mkdir($this->cacheDir, 0755, true);
        }
    }

    /**
     * Get client identifier (IP address)
     * @return string Client identifier
     */
    private function getClientIdentifier(): string {
        // Use REMOTE_ADDR only — HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP
        // are fully attacker-controlled request headers and must never be
        // trusted for rate-limiting purposes (an attacker can rotate them
        // freely to bypass the limiter). Nginx-level rate limiting also
        // uses $binary_remote_addr for the same reason. If this app is
        // ever placed behind a trusted reverse-proxy, IP extraction should
        // be handled at the nginx level, not here.
        return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    }

    /**
     * Get cache file path for a client
     * @param string $identifier Client identifier
     * @return string File path
     */
    private function getCacheFile($identifier) {
        return $this->cacheDir . '/' . md5($identifier) . '.json';
    }

    /**
     * Check and record a hit for an arbitrary string key.
     * Useful when the caller wants a compound key (e.g. slug + IP).
     *
     * @return bool True if allowed, false if rate limit exceeded
     */
    public function checkKey(string $key): bool {
        $file = $this->getCacheFile($key);

        $data = [];
        if (file_exists($file)) {
            $data = json_decode(file_get_contents($file), true) ?? [];
        }

        $now  = time();
        $data = array_values(array_filter($data, fn($ts) => ($now - $ts) < $this->timeWindow));

        if (count($data) >= $this->maxRequests) {
            return false;
        }

        $data[] = $now;
        if (is_dir($this->cacheDir) && is_writable($this->cacheDir)) {
            file_put_contents($file, json_encode($data));
        }

        return true;
    }

    /**
     * Check if client has exceeded rate limit
     * @return bool True if allowed, false if rate limit exceeded
     */
    public function check() {
        $identifier = $this->getClientIdentifier();
        $file = $this->getCacheFile($identifier);

        // Load existing request timestamps
        $data = [];
        if (file_exists($file)) {
            $content = file_get_contents($file);
            $data = json_decode($content, true) ?? [];
        }

        // Clean old entries outside time window
        $now = time();
        $data = array_filter($data, function($timestamp) use ($now) {
            return ($now - $timestamp) < $this->timeWindow;
        });

        // Check if limit exceeded
        if (count($data) >= $this->maxRequests) {
            return false;
        }

        // Add new request timestamp
        $data[] = $now;

        // Save updated data (silently skip if directory is not writable)
        if (is_dir($this->cacheDir) && is_writable($this->cacheDir)) {
            file_put_contents($file, json_encode($data));
        }

        return true;
    }

    /**
     * Get remaining requests for current client
     * @return int Number of requests remaining
     */
    public function getRemaining() {
        $identifier = $this->getClientIdentifier();
        $file = $this->getCacheFile($identifier);

        if (!file_exists($file)) {
            return $this->maxRequests;
        }

        $content = file_get_contents($file);
        $data = json_decode($content, true) ?? [];

        // Clean old entries
        $now = time();
        $data = array_filter($data, function($timestamp) use ($now) {
            return ($now - $timestamp) < $this->timeWindow;
        });

        return max(0, $this->maxRequests - count($data));
    }

    /**
     * Get time until rate limit resets
     * @return int Seconds until reset
     */
    public function getResetTime() {
        $identifier = $this->getClientIdentifier();
        $file = $this->getCacheFile($identifier);

        if (!file_exists($file)) {
            return 0;
        }

        $content = file_get_contents($file);
        $data = json_decode($content, true) ?? [];

        if (empty($data)) {
            return 0;
        }

        // Find oldest timestamp
        $oldest = min($data);
        $resetTime = $oldest + $this->timeWindow - time();

        return max(0, $resetTime);
    }

    /**
     * Clean up old cache files (run periodically)
     * Removes files that haven't been modified in 24 hours
     */
    public function cleanup() {
        $files = glob($this->cacheDir . '/*.json');
        $cutoff = time() - 86400; // 24 hours

        foreach ($files as $file) {
            if (filemtime($file) < $cutoff) {
                unlink($file);
            }
        }
    }

    /**
     * Send rate limit headers
     * Provides information about rate limits to clients
     */
    public function sendHeaders() {
        header('X-RateLimit-Limit: ' . $this->maxRequests);
        header('X-RateLimit-Remaining: ' . $this->getRemaining());
        header('X-RateLimit-Reset: ' . (time() + $this->getResetTime()));
    }
}
