<?php
declare(strict_types=1);

/**
 * Sessões, proteção contra envio forjado de formulário (CSRF), limite de tentativas de login e
 * cabeçalhos de segurança. As senhas nunca ficam em texto: só o hash (password_hash) no banco.
 */
final class Seguranca
{
    public static function https(): bool
    {
        return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
            || (($_SERVER['SERVER_PORT'] ?? '') === '443');
    }

    /** Cabeçalhos enviados em todas as páginas. */
    public static function cabecalhos(): void
    {
        header_remove('X-Powered-By');
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: DENY');
        header('Referrer-Policy: same-origin');
        header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
        header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; "
            . "connect-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'");
        header('Cache-Control: no-store');
        if (self::https()) {
            header('Strict-Transport-Security: max-age=31536000');
        }
    }

    /** Inicia a sessão de uma área ("aliado" ou "admin"), cada uma com o seu cookie. */
    public static function iniciaSessao(string $area, int $minutos): void
    {
        // sessões numa pasta própria, dentro da pasta privada: assim a limpeza automática do Ubuntu
        // (que apaga sessões paradas há 24 minutos) não derruba ninguém antes do tempo configurado
        $dir = Config::dirDados() . '/sessoes/' . ($area === 'admin' ? 'admin' : 'aliado');
        if (!is_dir($dir)) {
            @mkdir($dir, 0700, true);
        }
        session_save_path($dir);
        ini_set('session.gc_probability', '1');
        ini_set('session.gc_divisor', '50');
        ini_set('session.use_strict_mode', '1');
        ini_set('session.use_only_cookies', '1');
        ini_set('session.use_trans_sid', '0');
        ini_set('session.gc_maxlifetime', (string) ($minutos * 60));
        session_name($area === 'admin' ? 'sl_admin' : 'sl_aliado');
        session_set_cookie_params([
            'lifetime' => 0,
            'path' => '/',
            'secure' => self::https(),
            'httponly' => true,
            'samesite' => 'Strict',
        ]);
        session_start();
        $agora = time();
        if (isset($_SESSION['ultimo_uso']) && $agora - (int) $_SESSION['ultimo_uso'] > $minutos * 60) {
            $_SESSION = [];
            session_regenerate_id(true);
        }
        $_SESSION['ultimo_uso'] = $agora;
    }

    public static function encerraSessao(): void
    {
        $_SESSION = [];
        if (ini_get('session.use_cookies')) {
            $p = session_get_cookie_params();
            setcookie(session_name(), '', [
                'expires' => time() - 42000, 'path' => $p['path'], 'secure' => $p['secure'],
                'httponly' => true, 'samesite' => 'Strict',
            ]);
        }
        session_destroy();
    }

    public static function tokenCsrf(): string
    {
        if (empty($_SESSION['csrf'])) {
            $_SESSION['csrf'] = bin2hex(random_bytes(32));
        }
        return (string) $_SESSION['csrf'];
    }

    /** Confere o token do formulário (campo "csrf") ou do cabeçalho X-CSRF-Token. */
    public static function csrfValido(): bool
    {
        $enviado = $_POST['csrf'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
        return is_string($enviado) && !empty($_SESSION['csrf']) && hash_equals((string) $_SESSION['csrf'], $enviado);
    }

    /**
     * true se a pasta do sistema (app/) ou a de dados ficou dentro da pasta pública do site.
     * Nesse caso qualquer pessoa poderia baixar os arquivos, então as páginas se recusam a funcionar.
     */
    public static function instalacaoExposta(): bool
    {
        $doc = realpath((string) ($_SERVER['DOCUMENT_ROOT'] ?? ''));
        if ($doc === false || $doc === '') {
            return false;
        }
        $doc = rtrim($doc, '/') . '/';
        foreach ([Config::raiz() . '/app', Config::dirDados()] as $p) {
            $r = realpath($p);
            if ($r !== false && str_starts_with(rtrim($r, '/') . '/', $doc)) {
                return true;
            }
        }
        return false;
    }

    public static function ip(): string
    {
        return substr((string) ($_SERVER['REMOTE_ADDR'] ?? '?'), 0, 64);
    }

    // ---------------------------------------------------------------- limite de tentativas

    /** true se o usuário (ou o endereço IP) errou a senha demais nos últimos minutos. */
    public static function bloqueado(string $usuario): bool
    {
        $pdo = Banco::pdo();
        $janela = (int) Config::get('tentativas_janela_min') * 60;
        $max = (int) Config::get('tentativas_max');
        $desde = time() - $janela;
        $pdo->prepare('DELETE FROM tentativas WHERE momento < ?')->execute([time() - 86400]);
        $st = $pdo->prepare('SELECT COUNT(*) FROM tentativas WHERE chave = ? AND momento >= ?');
        $st->execute(['u:' . $usuario, $desde]);
        if ((int) $st->fetchColumn() >= $max) {
            return true;
        }
        // por endereço: um pouco mais folgado (vários aliados podem sair do mesmo escritório)
        $st->execute(['ip:' . self::ip(), $desde]);
        return (int) $st->fetchColumn() >= $max * 3;
    }

    public static function registraErro(string $usuario): void
    {
        $st = Banco::pdo()->prepare('INSERT INTO tentativas (chave, momento) VALUES (?, ?)');
        $agora = time();
        $st->execute(['u:' . $usuario, $agora]);
        $st->execute(['ip:' . self::ip(), $agora]);
    }

    public static function limpaErros(string $usuario): void
    {
        Banco::pdo()->prepare('DELETE FROM tentativas WHERE chave = ?')->execute(['u:' . $usuario]);
    }

    /** Senha forte o bastante? Devolve a mensagem do problema, ou null. */
    public static function problemaDaSenha(string $senha): ?string
    {
        if (mb_strlen($senha) < 10) {
            return 'A senha precisa ter pelo menos 10 caracteres.';
        }
        if (!preg_match('/[A-Za-z]/', $senha) || !preg_match('/\d/', $senha)) {
            return 'Use letras e números na senha.';
        }
        return null;
    }
}
