<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

use ShieldX\Storage\FileStorage;

final class EmergencyRecoveryThrottle
{
    private const WINDOW_SECONDS = 900;
    private const LOCK_SECONDS = 1800;
    private const TOKEN_LIMIT = 12;
    private const IP_LIMIT = 8;
    private const IDENTIFIER_LIMIT = 5;

    /** @var string */
    private $fileStoreDirectory;

    /** @var bool|null */
    private $useFileStore;

    public function __construct(?string $fileStoreDirectory = null, ?bool $useFileStore = null)
    {
        $this->fileStoreDirectory = is_string($fileStoreDirectory) && $fileStoreDirectory !== ''
            ? rtrim($fileStoreDirectory, '/\\')
            : '';
        $this->useFileStore = $useFileStore;
    }

    /**
     * @return array{locked:bool,reason:string,retry_after:int}
     */
    public function assess(string $token, string $ip, string $identifier): array
    {
        foreach ($this->lockKeys($token, $ip, $identifier) as $reason => $key) {
            $lock = $this->read($key);
            if (empty($lock)) {
                continue;
            }

            $expiresAt = (int) ($lock['expires_at'] ?? 0);
            if ($expiresAt > time()) {
                return [
                    'locked' => true,
                    'reason' => (string) $reason,
                    'retry_after' => max(1, $expiresAt - time()),
                ];
            }

            $this->delete($key);
        }

        return [
            'locked' => false,
            'reason' => '',
            'retry_after' => 0,
        ];
    }

    /**
     * @return array{locked:bool,reason:string,retry_after:int}
     */
    public function recordFailure(string $token, string $ip, string $identifier): array
    {
        $buckets = [
            'token' => [$this->counterKey('token', $token), self::TOKEN_LIMIT],
            'ip' => [$this->counterKey('ip', $ip), self::IP_LIMIT],
            'identifier' => [$this->counterKey('identifier', $identifier), self::IDENTIFIER_LIMIT],
        ];

        foreach ($buckets as $reason => $bucket) {
            [$key, $limit] = $bucket;
            if ($key === '') {
                continue;
            }

            $record = $this->updateCounter($key, self::WINDOW_SECONDS);

            if ((int) $record['count'] >= (int) $limit) {
                $this->write($this->lockKey((string) $reason, (string) $bucket[0]), [
                    'expires_at' => time() + self::LOCK_SECONDS,
                    'count' => (int) $record['count'],
                ], self::LOCK_SECONDS);
            }
        }

        return $this->assess($token, $ip, $identifier);
    }

    public function recordSuccess(string $token, string $ip, string $identifier): void
    {
        foreach ($this->counterKeys($token, $ip, $identifier) as $key) {
            $this->delete($key);
            $this->delete($this->lockKeyFromCounter($key));
        }
    }

    /**
     * @return array<string,string>
     */
    private function lockKeys(string $token, string $ip, string $identifier): array
    {
        $keys = [];
        foreach ($this->counterKeys($token, $ip, $identifier) as $key) {
            if (strpos($key, ':token:') !== false) {
                $keys['token'] = $this->lockKeyFromCounter($key);
            } elseif (strpos($key, ':ip:') !== false) {
                $keys['ip'] = $this->lockKeyFromCounter($key);
            } elseif (strpos($key, ':identifier:') !== false) {
                $keys['identifier'] = $this->lockKeyFromCounter($key);
            }
        }

        return $keys;
    }

    /**
     * @return array<int,string>
     */
    private function counterKeys(string $token, string $ip, string $identifier): array
    {
        return array_values(array_filter([
            $this->counterKey('token', $token),
            $this->counterKey('ip', $ip),
            $this->counterKey('identifier', $identifier),
        ]));
    }

    private function counterKey(string $scope, string $value): string
    {
        $value = trim(strtolower($value));
        if ($value === '') {
            return '';
        }

        return 'shieldx_recovery:' . $scope . ':' . hash_hmac('sha256', $value, self::salt());
    }

    private function lockKeyFromCounter(string $counterKey): string
    {
        return str_replace('shieldx_recovery:', 'shieldx_recovery_lock:', $counterKey);
    }

    private function lockKey(string $scope, string $counterKey): string
    {
        return $this->lockKeyFromCounter($counterKey);
    }

    /**
     * @return array<string,mixed>
     */
    private function read(string $key): array
    {
        if ($key === '') {
            return [];
        }

        if (!$this->shouldUseFileStore()) {
            throw $this->storageUnavailable();
        }

        return $this->readFileRecord($key);
    }

    /**
     * @param array<string,mixed> $value
     */
    private function write(string $key, array $value, int $ttl): void
    {
        if ($key === '') {
            return;
        }

        if (!isset($value['expires_at'])) {
            $value['expires_at'] = time() + max(1, $ttl);
        }

        if (!$this->shouldUseFileStore()) {
            throw $this->storageUnavailable();
        }

        $this->writeFileRecord($key, $value);
    }

    private function delete(string $key): void
    {
        if ($key === '') {
            return;
        }

        if (!$this->shouldUseFileStore()) {
            throw $this->storageUnavailable();
        }

        $this->deleteFileRecord($key);
    }

    /**
     * @return array<string,mixed>
     */
    private function updateCounter(string $key, int $ttl): array
    {
        if (!$this->shouldUseFileStore()) {
            throw $this->storageUnavailable();
        }

        return $this->updateFileCounter($key, $ttl);
    }

    /**
     * @return array<string,mixed>
     */
    private function updateFileCounter(string $key, int $ttl): array
    {
        $path = $this->fileRecordPath($key);
        $this->ensureFileStoreDirectory(dirname($path));

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Private SHIELD-X throttle state needs a local lock file for atomic recovery failure counters.
        $handle = fopen($path, 'c+b');
        if (!is_resource($handle)) {
            throw $this->storageUnavailable();
        }

        $record = [];
        $locked = false;

        try {
            if (!flock($handle, LOCK_EX)) {
                throw $this->storageUnavailable();
            }
            $locked = true;
            rewind($handle);
            $raw = stream_get_contents($handle);
            if ($raw === false) {
                throw $this->storageUnavailable();
            }
            if ($raw !== '') {
                $decoded = \ShieldX\Support\Json::decodeArray($raw);
                if (!is_array($decoded)) {
                    throw $this->storageUnavailable();
                }
                $record = $decoded;
            }

            $expiresAt = (int) ($record['expires_at'] ?? 0);
            if (empty($record) || $expiresAt <= time()) {
                $record = [
                    'count' => 0,
                    'started_at' => time(),
                    'expires_at' => time() + max(1, $ttl),
                ];
            }

            $record['count'] = (int) ($record['count'] ?? 0) + 1;
            $record['last_failure_at'] = time();
            $json = wp_json_encode($record);
            if (!is_string($json) || !ftruncate($handle, 0)) {
                throw $this->storageUnavailable();
            }
            rewind($handle);
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Required for the private flock-protected recovery throttle counter file.
            $written = fwrite($handle, $json);
            if (!is_int($written) || $written !== strlen($json) || !fflush($handle)) {
                throw $this->storageUnavailable();
            }
        } finally {
            if ($locked) {
                flock($handle, LOCK_UN);
            }
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Completes the private flock-protected throttle file operation.
            fclose($handle);
        }

        return $record;
    }

    /**
     * @return array<string,mixed>
     */
    private function readFileRecord(string $key): array
    {
        $path = $this->fileRecordPath($key);
        $this->ensureFileStoreDirectory(dirname($path));
        if (!is_file($path)) {
            return [];
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Private SHIELD-X throttle records are read with a shared lock to avoid partial reads.
        $handle = fopen($path, 'rb');
        if (!is_resource($handle)) {
            throw $this->storageUnavailable();
        }

        $locked = false;

        try {
            if (!flock($handle, LOCK_SH)) {
                throw $this->storageUnavailable();
            }
            $locked = true;
            $raw = stream_get_contents($handle);
            if (!is_string($raw) || $raw === '') {
                throw $this->storageUnavailable();
            }
            $decoded = \ShieldX\Support\Json::decodeArray($raw);
            if (!is_array($decoded)) {
                throw $this->storageUnavailable();
            }
        } finally {
            if ($locked) {
                flock($handle, LOCK_UN);
            }
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Completes the private shared-lock throttle file read.
            fclose($handle);
        }

        return $decoded;
    }

    /**
     * @param array<string,mixed> $value
     */
    private function writeFileRecord(string $key, array $value): void
    {
        $path = $this->fileRecordPath($key);
        $this->ensureFileStoreDirectory(dirname($path));

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Private SHIELD-X throttle records are written under an exclusive lock.
        $handle = fopen($path, 'c+b');
        if (!is_resource($handle)) {
            throw $this->storageUnavailable();
        }

        $locked = false;

        try {
            if (!flock($handle, LOCK_EX)) {
                throw $this->storageUnavailable();
            }
            $locked = true;
            $json = wp_json_encode($value);
            if (!is_string($json) || !ftruncate($handle, 0)) {
                throw $this->storageUnavailable();
            }
            rewind($handle);
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Required for the private flock-protected recovery throttle state file.
            $written = fwrite($handle, $json);
            if (!is_int($written) || $written !== strlen($json) || !fflush($handle)) {
                throw $this->storageUnavailable();
            }
        } finally {
            if ($locked) {
                flock($handle, LOCK_UN);
            }
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Completes the private flock-protected throttle file write.
            fclose($handle);
        }
    }

    private function deleteFileRecord(string $key): void
    {
        $path = $this->fileRecordPath($key);
        $this->ensureFileStoreDirectory(dirname($path));
        if (is_file($path)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Removes private SHIELD-X throttle state after successful recovery verification.
            if (!unlink($path)) {
                throw $this->storageUnavailable();
            }
        }
    }

    private function fileRecordPath(string $key): string
    {
        $directory = $this->fileStoreDirectory();
        return $directory . '/' . hash('sha256', $key) . '.json';
    }

    private function fileStoreDirectory(): string
    {
        if ($this->fileStoreDirectory !== '') {
            return $this->fileStoreDirectory;
        }

        try {
            $baseDir = FileStorage::fromWordPress()->baseDir();
            if ($baseDir === '') {
                throw new \RuntimeException('SHIELD-X storage is unavailable.');
            }
            $this->fileStoreDirectory = rtrim($baseDir, '/\\') . '/cache/recovery-throttle';
        } catch (\Throwable $error) {
            throw $this->storageUnavailable();
        }

        return $this->fileStoreDirectory;
    }

    private function ensureFileStoreDirectory(string $directory): void
    {
        if ($directory === '' || (file_exists($directory) && !is_dir($directory))) {
            throw $this->storageUnavailable();
        }

        if (is_dir($directory)) {
            if (!$this->directoryWritable($directory)) {
                throw $this->storageUnavailable();
            }
            return;
        }

        if (function_exists('wp_mkdir_p')) {
            $created = wp_mkdir_p($directory);
        } else {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- Creates private SHIELD-X throttle state directory outside public request paths.
            $created = mkdir($directory, 0750, true);
        }

        if (!$created && !is_dir($directory)) {
            throw $this->storageUnavailable();
        }

        clearstatcache(true, $directory);
        if (!is_dir($directory) || !$this->directoryWritable($directory)) {
            throw $this->storageUnavailable();
        }
    }

    private function directoryWritable(string $directory): bool
    {
        if (!is_dir($directory)) {
            return false;
        }

        if (function_exists('wp_is_writable')) {
            return wp_is_writable($directory);
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Unit tests can run without WordPress; production uses wp_is_writable() above.
        return is_writable($directory);
    }

    private function shouldUseFileStore(): bool
    {
        return $this->useFileStore !== false;
    }

    private function storageUnavailable(): EmergencyRecoveryThrottleStorageUnavailableException
    {
        return new EmergencyRecoveryThrottleStorageUnavailableException('Emergency recovery rate-limit storage is unavailable.');
    }

    private static function salt(): string
    {
        $salt = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';
        if ($salt === '') {
            throw new \RuntimeException('WordPress secure authentication salts are required for SHIELD-X emergency recovery throttling.');
        }

        return $salt;
    }
}
