<?php

declare(strict_types=1);

final class Security
{
    public static function isHttps(): bool
    {
        return (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off')
            || (($_SERVER['SERVER_PORT'] ?? null) === '443');
    }

    public static function startSession(?string $name = null): void
    {
        if (PHP_SAPI === 'cli' || session_status() === PHP_SESSION_ACTIVE) {
            return;
        }

        ini_set('session.use_strict_mode', '1');
        ini_set('session.use_only_cookies', '1');
        ini_set('session.cookie_httponly', '1');
        ini_set('session.cookie_samesite', 'Lax');
        ini_set('session.gc_maxlifetime', (string) (defined('SESSION_IDLE_TIMEOUT') ? SESSION_IDLE_TIMEOUT : 7200));

        if ($name !== null && $name !== '') {
            session_name($name);
        }

        session_set_cookie_params([
            'lifetime' => 0,
            'path' => '/',
            'domain' => '',
            'secure' => self::isHttps(),
            'httponly' => true,
            'samesite' => 'Lax',
        ]);

        session_start();
    }

    public static function destroySession(): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            return;
        }

        $_SESSION = [];
        if (ini_get('session.use_cookies')) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', [
                'expires' => time() - 42000,
                'path' => $params['path'] ?: '/',
                'domain' => $params['domain'] ?: '',
                'secure' => (bool) $params['secure'],
                'httponly' => (bool) $params['httponly'],
                'samesite' => $params['samesite'] ?? 'Lax',
            ]);
        }
        session_destroy();
    }

    public static function headers(bool $json = false): void
    {
        if (headers_sent()) {
            return;
        }

        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: SAMEORIGIN');
        header('Referrer-Policy: same-origin');
        header('Permissions-Policy: geolocation=(), camera=(), microphone=()');
        header('Cross-Origin-Opener-Policy: same-origin');
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
        header('Pragma: no-cache');
        if ($json) {
            header('Content-Type: application/json; charset=utf-8');
        }
    }

    public static function csrfToken(): string
    {
        self::startSession(defined('SESSION_NAME') ? SESSION_NAME : null);
        if (empty($_SESSION['_csrf']) || !is_string($_SESSION['_csrf'])) {
            $_SESSION['_csrf'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['_csrf'];
    }

    public static function csrfField(): string
    {
        return '<input type="hidden" name="_csrf" value="'
            . htmlspecialchars(self::csrfToken(), ENT_QUOTES, 'UTF-8')
            . '">';
    }

    public static function csrfFromRequest(): string
    {
        $header = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
        if (is_string($header) && $header !== '') {
            return $header;
        }
        return is_string($_POST['_csrf'] ?? null) ? $_POST['_csrf'] : '';
    }

    public static function validateCsrf(?string $token = null): bool
    {
        self::startSession(defined('SESSION_NAME') ? SESSION_NAME : null);
        $known = $_SESSION['_csrf'] ?? '';
        $candidate = $token ?? self::csrfFromRequest();
        return is_string($known)
            && $known !== ''
            && $candidate !== ''
            && hash_equals($known, $candidate);
    }

    public static function requireCsrf(): void
    {
        if (self::validateCsrf()) {
            return;
        }

        if (self::expectsJson()) {
            self::json([
                'success' => false,
                'message' => 'La sesión de seguridad venció. Recarga la página e inténtalo nuevamente.',
            ], 419);
        }

        http_response_code(419);
        exit('La sesión de seguridad venció. Recarga la página.');
    }

    public static function expectsJson(): bool
    {
        $requestedWith = strtolower((string) ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''));
        $accept = strtolower((string) ($_SERVER['HTTP_ACCEPT'] ?? ''));
        return $requestedWith === 'xmlhttprequest'
            || str_contains($accept, 'application/json')
            || str_contains(strtolower((string) ($_SERVER['CONTENT_TYPE'] ?? '')), 'application/json');
    }

    public static function json(array $payload, int $status = 200): never
    {
        self::headers(true);
        http_response_code($status);
        echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public static function clientIp(): string
    {
        $ip = (string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
        return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
    }

    public static function fingerprint(): string
    {
        return hash('sha256', (string) ($_SERVER['HTTP_USER_AGENT'] ?? 'unknown'));
    }

    /**
     * Limitador simple con archivo bloqueado. Si el servidor no permite escribir,
     * usa la sesión como respaldo.
     *
     * @return array{allowed:bool,retry_after:int,remaining:int}
     */
    public static function rateLimit(string $bucket, int $maxAttempts, int $windowSeconds): array
    {
        $key = hash('sha256', $bucket);
        $dir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'afenixx-rate-limit';
        $now = time();

        if ((is_dir($dir) || @mkdir($dir, 0700, true)) && is_writable($dir)) {
            $file = $dir . DIRECTORY_SEPARATOR . $key . '.json';
            $handle = @fopen($file, 'c+');
            if ($handle !== false && flock($handle, LOCK_EX)) {
                $raw = stream_get_contents($handle);
                $data = json_decode($raw ?: '{}', true);
                $start = (int) ($data['start'] ?? $now);
                $count = (int) ($data['count'] ?? 0);
                if ($start + $windowSeconds <= $now) {
                    $start = $now;
                    $count = 0;
                }
                $count++;
                $allowed = $count <= $maxAttempts;
                $retry = max(0, ($start + $windowSeconds) - $now);
                $remaining = max(0, $maxAttempts - $count);

                ftruncate($handle, 0);
                rewind($handle);
                fwrite($handle, json_encode(['start' => $start, 'count' => $count]));
                fflush($handle);
                flock($handle, LOCK_UN);
                fclose($handle);

                return ['allowed' => $allowed, 'retry_after' => $retry, 'remaining' => $remaining];
            }
            if (is_resource($handle)) {
                fclose($handle);
            }
        }

        self::startSession(defined('SESSION_NAME') ? SESSION_NAME : null);
        $_SESSION['_rate'] ??= [];
        $data = $_SESSION['_rate'][$key] ?? ['start' => $now, 'count' => 0];
        if (($data['start'] + $windowSeconds) <= $now) {
            $data = ['start' => $now, 'count' => 0];
        }
        $data['count']++;
        $_SESSION['_rate'][$key] = $data;

        return [
            'allowed' => $data['count'] <= $maxAttempts,
            'retry_after' => max(0, ($data['start'] + $windowSeconds) - $now),
            'remaining' => max(0, $maxAttempts - $data['count']),
        ];
    }

    public static function clearRateLimit(string $bucket): void
    {
        $key = hash('sha256', $bucket);
        $file = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
            . DIRECTORY_SEPARATOR . 'afenixx-rate-limit'
            . DIRECTORY_SEPARATOR . $key . '.json';
        if (is_file($file)) {
            @unlink($file);
        }
        if (session_status() === PHP_SESSION_ACTIVE) {
            unset($_SESSION['_rate'][$key]);
        }
    }
}
