<?php
declare(strict_types=1);

namespace App\Security;

/**
 * File-based fallback limiter.
 *
 * It preserves the old behavior while adding proper file locking and
 * directory-level isolation by scope/identity/window.
 */
final class FileRateLimiter implements RateLimiterInterface
{
    private string $storageDir;

    public function __construct(string $storageDir)
    {
        $this->storageDir = rtrim(str_replace('\\', '/', $storageDir), '/');
    }

    public function consume(string $scope, int $limit, int $windowSec, string $identity = ''): array
    {
        $scope = trim($scope);
        if ($scope === '' || $limit < 1 || $windowSec < 1) {
            return ['limited' => false, 'remaining' => max(0, $limit), 'retry_after' => 0];
        }

        $identity = trim($identity);
        if ($identity === '') {
            $identity = 'global';
        }

        $now = time();
        $bucket = (int)floor($now / $windowSec);
        $expiresAt = ($bucket + 1) * $windowSec;

        if (!is_dir($this->storageDir) && !@mkdir($this->storageDir, 0755, true) && !is_dir($this->storageDir)) {
            return [
                'limited' => false,
                'remaining' => max(0, $limit - 1),
                'retry_after' => max(0, $expiresAt - $now),
            ];
        }

        $file = $this->storageDir . '/' . hash('sha256', $scope . '|' . $identity . '|' . $bucket) . '.json';
        $fp = @fopen($file, 'c+');
        if ($fp === false) {
            return [
                'limited' => false,
                'remaining' => max(0, $limit - 1),
                'retry_after' => max(0, $expiresAt - $now),
            ];
        }

        $count = 0;
        try {
            if (!@flock($fp, LOCK_EX)) {
                return [
                    'limited' => false,
                    'remaining' => max(0, $limit - 1),
                    'retry_after' => max(0, $expiresAt - $now),
                ];
            }

            $raw = stream_get_contents($fp);
            $row = is_string($raw) ? json_decode($raw, true) : null;
            if (is_array($row)) {
                $count = (int)($row['count'] ?? 0);
                $savedExpires = (int)($row['expires_at'] ?? 0);
                if ($savedExpires > 0) {
                    $expiresAt = $savedExpires;
                }
            }

            if ($now >= $expiresAt) {
                $count = 0;
                $expiresAt = ($bucket + 1) * $windowSec;
            }

            if ($count >= $limit) {
                @flock($fp, LOCK_UN);
                return [
                    'limited' => true,
                    'remaining' => 0,
                    'retry_after' => max(1, $expiresAt - $now),
                ];
            }

            $count++;
            rewind($fp);
            @ftruncate($fp, 0);
            @fwrite($fp, json_encode([
                'count' => $count,
                'expires_at' => $expiresAt,
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
            @fflush($fp);
            @flock($fp, LOCK_UN);
        } finally {
            @fclose($fp);
        }

        return [
            'limited' => false,
            'remaining' => max(0, $limit - $count),
            'retry_after' => max(0, $expiresAt - $now),
        ];
    }
}
