<?php
/**
 * Antara — rate limiting and lockout for the authentication endpoints.
 *
 * - failedAttempts and lockedUntil are stored on the user row when a username
 *   resolves to an existing account.
 * - For unknown usernames or IP-only buckets, we keep counters in rate.enc.
 * - GC: entries older than (lockout_minutes * 4) are pruned on every write.
 */

require_once __DIR__ . '/../auth/store.php';

final class AntaraRateLimit
{
    public static function key(string $ip, string $login): string
    {
        $ip = (string)$ip;
        $login = strtolower(trim((string)$login));
        return hash('sha256', $ip . '|' . $login);
    }

    /**
     * Returns [now-locked?, retryAfterSeconds]. When the bucket exceeds the
     * threshold, locks are imposed for `lockout_minutes` minutes.
     */
    public static function checkBeforeAttempt(string $ip, string $login, array $cfg): array
    {
        $now = time();
        $threshold = (int)($cfg['max_failed_attempts'] ?? 5);
        $lockoutSec = ((int)($cfg['lockout_minutes'] ?? 15)) * 60;

        // user-bucket lockout
        $user = AntaraStore::findUserByLogin($login);
        if ($user) {
            $lockedUntil = (int)strtotime((string)($user['lockedUntil'] ?? '')) ?: 0;
            if ($lockedUntil > $now) {
                return [true, $lockedUntil - $now];
            }
        }

        // ip+login bucket lockout
        $blob = AntaraStore::rateRead();
        $key = self::key($ip, $login);
        $entry = $blob['entries'][$key] ?? null;
        if ($entry) {
            $lockedUntil = (int)($entry['lockedUntil'] ?? 0);
            if ($lockedUntil > $now) {
                return [true, $lockedUntil - $now];
            }
        }
        return [false, 0];
    }

    public static function recordFailure(string $ip, string $login, array $cfg): void
    {
        $now = time();
        $threshold = (int)($cfg['max_failed_attempts'] ?? 5);
        $lockoutSec = ((int)($cfg['lockout_minutes'] ?? 15)) * 60;

        // user bucket
        $user = AntaraStore::findUserByLogin($login);
        if ($user) {
            $fa = (int)($user['failedAttempts'] ?? 0) + 1;
            $patch = ['id' => $user['id'], 'failedAttempts' => $fa];
            if ($fa >= $threshold) {
                $patch['lockedUntil'] = gmdate('Y-m-d\TH:i:s\Z', $now + $lockoutSec);
            }
            AntaraStore::upsertUser($patch, $user['id']);
        }

        // ip bucket
        AntaraStore::rateTransact(function (&$blob) use ($ip, $login, $now, $threshold, $lockoutSec) {
            $blob['entries'] = $blob['entries'] ?? [];
            $key = self::key($ip, $login);
            $e = $blob['entries'][$key] ?? ['count' => 0, 'firstAt' => $now, 'lockedUntil' => 0];
            $e['count'] = (int)$e['count'] + 1;
            $e['lastAt'] = $now;
            if ($e['count'] >= $threshold) {
                $e['lockedUntil'] = $now + $lockoutSec;
            }
            $blob['entries'][$key] = $e;

            // GC old entries
            $cutoff = $now - ($lockoutSec * 4);
            foreach ($blob['entries'] as $k => $v) {
                if ((int)($v['lastAt'] ?? 0) < $cutoff && (int)($v['lockedUntil'] ?? 0) < $now) {
                    unset($blob['entries'][$k]);
                }
            }
        });
    }

    public static function recordSuccess(string $ip, string $login, ?string $userId = null): void
    {
        if ($userId) {
            $now = gmdate('Y-m-d\TH:i:s\Z');
            AntaraStore::upsertUser([
                'id' => $userId,
                'failedAttempts' => 0,
                'lockedUntil'    => null,
                'lastLoginAt'    => $now,
            ], $userId);
        }
        AntaraStore::rateTransact(function (&$blob) use ($ip, $login) {
            unset($blob['entries'][self::key($ip, $login)]);
        });
    }
}
