<?php

namespace App\Core;

/**
 * ITSWEBER CMS – File-based Rate Limiter
 *
 * Lightweight throttle for login and other sensitive endpoints.
 * Stores attempt counters as JSON files in storage/cache/throttle/.
 * No external dependencies (Redis, Memcached) required.
 */
class RateLimiter
{
    private string $storageDir;

    /** Maximum failed attempts before lockout. */
    private int $maxAttempts;

    /** Lockout duration in seconds after max attempts exceeded. */
    private int $lockoutSeconds;

    /** Time window in seconds within which attempts are counted. */
    private int $windowSeconds;

    public function __construct(
        string $storageDir,
        int    $maxAttempts   = 5,
        int    $lockoutSeconds = 900,  // 15 minutes
        int    $windowSeconds  = 600   // 10 minutes
    ) {
        $this->storageDir    = rtrim($storageDir, '/\\') . '/throttle';
        $this->maxAttempts   = $maxAttempts;
        $this->lockoutSeconds = $lockoutSeconds;
        $this->windowSeconds  = $windowSeconds;

        if (!is_dir($this->storageDir)) {
            @mkdir($this->storageDir, 0755, true);
        }
    }

    /**
     * Check if the given key is currently rate-limited (locked out).
     */
    public function isLocked(string $key): bool
    {
        $data = $this->load($key);
        if ($data === null) {
            return false;
        }

        // Check if locked out
        if ($data['locked_until'] > 0 && $data['locked_until'] > time()) {
            return true;
        }

        return false;
    }

    /**
     * Get remaining lockout seconds (0 if not locked).
     */
    public function lockoutRemaining(string $key): int
    {
        $data = $this->load($key);
        if ($data === null || $data['locked_until'] <= 0) {
            return 0;
        }
        $remaining = $data['locked_until'] - time();
        return max(0, $remaining);
    }

    /**
     * Record a failed attempt. Returns true if now locked out.
     */
    public function recordFailure(string $key): bool
    {
        $data = $this->load($key) ?? $this->emptyRecord();
        $now  = time();

        // Reset if window has expired
        if ($data['window_start'] > 0 && ($now - $data['window_start']) > $this->windowSeconds) {
            $data = $this->emptyRecord();
        }

        if ($data['window_start'] === 0) {
            $data['window_start'] = $now;
        }

        $data['attempts']++;
        $data['last_attempt'] = $now;

        // Lock out if threshold exceeded
        if ($data['attempts'] >= $this->maxAttempts) {
            $data['locked_until'] = $now + $this->lockoutSeconds;
        }

        $this->save($key, $data);

        return $data['locked_until'] > $now;
    }

    /**
     * Clear attempts after successful login.
     */
    public function clear(string $key): void
    {
        $file = $this->filePath($key);
        if (is_file($file)) {
            @unlink($file);
        }
    }

    /**
     * Purge expired throttle files (housekeeping).
     */
    public function purgeExpired(): void
    {
        if (!is_dir($this->storageDir)) {
            return;
        }
        $now = time();
        foreach (glob($this->storageDir . '/*.json') as $file) {
            $data = @json_decode((string) @file_get_contents($file), true);
            if (!is_array($data)) {
                @unlink($file);
                continue;
            }
            // Remove if both window and lockout have expired
            $windowExpired  = ($data['window_start'] ?? 0) + $this->windowSeconds < $now;
            $lockoutExpired = ($data['locked_until'] ?? 0) < $now;
            if ($windowExpired && $lockoutExpired) {
                @unlink($file);
            }
        }
    }

    // ─── Private helpers ─────────────────────────────────────

    private function emptyRecord(): array
    {
        return [
            'attempts'     => 0,
            'window_start' => 0,
            'last_attempt' => 0,
            'locked_until' => 0,
        ];
    }

    private function filePath(string $key): string
    {
        return $this->storageDir . '/' . hash('sha256', $key) . '.json';
    }

    private function load(string $key): ?array
    {
        $file = $this->filePath($key);
        if (!is_file($file)) {
            return null;
        }
        $data = @json_decode((string) @file_get_contents($file), true);
        return is_array($data) ? $data : null;
    }

    private function save(string $key, array $data): void
    {
        $file = $this->filePath($key);
        @file_put_contents($file, json_encode($data), LOCK_EX);
    }
}
