<?php
/** AURA FARMING — Funções da Etapa 2 */

// ============================================================
// Utilidades gerais
// ============================================================
function esc(?string $v): string { return htmlspecialchars((string)($v ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'); }
function sanitize(?string $v): string { return trim(strip_tags((string)($v ?? ''))); }

function clientIp(): string
{
    return substr((string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'), 0, 45);
}

function isHttps(): bool
{
    return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
}

function uuidV4(): string
{
    $data = random_bytes(16);
    $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
    $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

function cookieOptions(int $expires): array
{
    return ['expires'=>$expires, 'path'=>'/', 'secure'=>isHttps(), 'httponly'=>true, 'samesite'=>'Lax'];
}

function deviceUuid(): string
{
    $headerDevice = (string)($_SERVER['HTTP_X_DEVICE_ID'] ?? '');
    $existing = preg_replace('/[^a-zA-Z0-9\-]/', '', $headerDevice ?: (string)($_COOKIE[DEVICE_COOKIE_NAME] ?? ''));
    if (strlen($existing) >= 20) return $existing;
    $uuid = uuidV4();
    setcookie(DEVICE_COOKIE_NAME, $uuid, cookieOptions(time() + 86400 * 365 * 5));
    $_COOKIE[DEVICE_COOKIE_NAME] = $uuid;
    return $uuid;
}

function detectarPlataforma(string $ua): string
{
    $ua = strtolower($ua);
    return match (true) {
        str_contains($ua, 'android') => 'Android',
        str_contains($ua, 'iphone'), str_contains($ua, 'ipad') => 'iOS',
        str_contains($ua, 'windows') => 'Windows',
        str_contains($ua, 'macintosh') => 'macOS',
        str_contains($ua, 'linux') => 'Linux',
        default => 'Desconhecido',
    };
}

function detectarNavegador(string $ua): string
{
    return match (true) {
        str_contains($ua, 'Edg/') => 'Edge',
        str_contains($ua, 'OPR/'), str_contains($ua, 'Opera') => 'Opera',
        str_contains($ua, 'Chrome/') => 'Chrome',
        str_contains($ua, 'Firefox/') => 'Firefox',
        str_contains($ua, 'Safari/') => 'Safari',
        default => 'Navegador',
    };
}

// ============================================================
// CSRF
// ============================================================
function gerarCsrfToken(): string
{
    if (empty($_SESSION[CSRF_TOKEN_NAME])) $_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
    return $_SESSION[CSRF_TOKEN_NAME];
}
function validarCsrfToken(?string $token): bool
{
    return !empty($token) && !empty($_SESSION[CSRF_TOKEN_NAME]) && hash_equals($_SESSION[CSRF_TOKEN_NAME], $token);
}
function camposCsrf(): string
{
    return '<input type="hidden" name="' . CSRF_TOKEN_NAME . '" value="' . esc(gerarCsrfToken()) . '">';
}

// ============================================================
// Usuário, perfil e ID
// ============================================================
function gerarAuraId(PDO $pdo): string
{
    do {
        $id = 'AURA-' . random_int(1000, 9999);
        $st = $pdo->prepare('SELECT id FROM af_usuarios WHERE aura_id=? LIMIT 1');
        $st->execute([$id]);
    } while ($st->fetchColumn());
    return $id;
}

function usernameDisponivel(PDO $pdo, string $base): string
{
    $base = strtolower(preg_replace('/[^a-zA-Z0-9_]/', '', iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $base) ?: $base));
    $base = substr($base ?: 'farmer', 0, 24);
    if (strlen($base) < 3) $base = 'farmer';
    $candidate = $base;
    for ($i=0; $i<100; $i++) {
        $st = $pdo->prepare('SELECT id FROM af_usuarios WHERE username=? LIMIT 1');
        $st->execute([$candidate]);
        if (!$st->fetchColumn()) return $candidate;
        $candidate = substr($base, 0, 24) . random_int(100, 9999);
    }
    return 'farmer_' . bin2hex(random_bytes(4));
}

function garantirPerfil(PDO $pdo, int $usuarioId): void
{
    $st = $pdo->prepare('INSERT IGNORE INTO af_perfis (usuario_id) VALUES (?)');
    $st->execute([$usuarioId]);
}

function criarUsuarioLocal(PDO $pdo, array $dados, ?array $avatar = null): array
{
    $pdo->beginTransaction();
    try {
        $auraId = gerarAuraId($pdo);
        $foto = null;
        $hash = password_hash((string)$dados['senha'], PASSWORD_DEFAULT);
        $st = $pdo->prepare('INSERT INTO af_usuarios (nome,username,email,senha,aura_id,auth_provider,data_cadastro,atualizado_em) VALUES (?,?,?,?,?,\'local\',NOW(),NOW())');
        $st->execute([$dados['nome'],$dados['username'],strtolower($dados['email']),$hash,$auraId]);
        $id = (int)$pdo->lastInsertId();
        garantirPerfil($pdo, $id);
        if (!empty($dados['data_nascimento'])) {
            afRegistrarDeclaracaoIdade(
                $pdo, $id, (string)$dados['data_nascimento'],
                (string)($dados['pais_conta'] ?? 'Brasil'),
                (string)($dados['uso_conta'] ?? 'player'), 'cadastro_local'
            );
        }
        if ($avatar && ($avatar['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
            $foto = processarAvatar($avatar, 'av_' . $id);
            if (!$foto) throw new RuntimeException('Foto inválida. Use JPG, PNG ou WEBP de até 5 MB.');
            $pdo->prepare('UPDATE af_usuarios SET foto_perfil=? WHERE id=?')->execute([$foto,$id]);
        }
        criarNotificacao($pdo,$id,'welcome','Bem-vindo ao Aura Farming','Seu ID exclusivo é ' . $auraId . '.');
        $pdo->commit();
        $st = $pdo->prepare('SELECT * FROM af_usuarios WHERE id=?');
        $st->execute([$id]);
        return $st->fetch();
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        if ($foto) @unlink(UPLOAD_PATH . $foto);
        throw $e;
    }
}

function criarOuVincularUsuarioSocial(PDO $pdo, string $provider, string $providerId, string $email, string $nome, ?string $foto = null): array
{
    $st = $pdo->prepare('SELECT u.* FROM af_oauth_contas o JOIN af_usuarios u ON u.id=o.usuario_id WHERE o.provider=? AND o.provider_user_id=? LIMIT 1');
    $st->execute([$provider,$providerId]);
    if ($u = $st->fetch()) return $u;

    $pdo->beginTransaction();
    try {
        $st = $pdo->prepare('SELECT * FROM af_usuarios WHERE email=? LIMIT 1');
        $st->execute([strtolower($email)]);
        $u = $st->fetch();
        if (!$u) {
            $username = usernameDisponivel($pdo, explode('@',$email)[0] ?: $nome);
            $auraId = gerarAuraId($pdo);
            $senha = password_hash(bin2hex(random_bytes(32)), PASSWORD_DEFAULT);
            $ins = $pdo->prepare('INSERT INTO af_usuarios (nome,username,email,senha,foto_externa,aura_id,auth_provider,email_verificado_em,data_cadastro,atualizado_em) VALUES (?,?,?,?,?,?,?,NOW(),NOW(),NOW())');
            $ins->execute([$nome ?: $username,$username,strtolower($email),$senha,$foto,$auraId,$provider]);
            $uid = (int)$pdo->lastInsertId();
            garantirPerfil($pdo,$uid);
            criarNotificacao($pdo,$uid,'welcome','Bem-vindo ao Aura Farming','Sua conta foi criada com ' . ucfirst($provider) . '.');
        } else {
            $uid = (int)$u['id'];
        }
        $ins = $pdo->prepare('INSERT INTO af_oauth_contas (usuario_id,provider,provider_user_id,email,criada_em) VALUES (?,?,?,?,NOW())');
        $ins->execute([$uid,$provider,$providerId,strtolower($email)]);
        $pdo->commit();
        $st = $pdo->prepare('SELECT * FROM af_usuarios WHERE id=?');
        $st->execute([$uid]);
        return $st->fetch();
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $e;
    }
}

function recarregarSessaoUsuario(PDO $pdo, int $usuarioId): void
{
    $st = $pdo->prepare('SELECT u.*,p.bio,p.cidade,p.estado,p.pais,p.perfil_privado,a.data_nascimento,a.pais_residencia,a.tipo_conta,a.status_idade,a.idade_declarada,a.declarado_em,a.verificado_em FROM af_usuarios u LEFT JOIN af_perfis p ON p.usuario_id=u.id LEFT JOIN af_user_age_profiles a ON a.usuario_id=u.id WHERE u.id=? LIMIT 1');
    $st->execute([$usuarioId]);
    if ($u=$st->fetch()) {
        unset($u['senha'],$u['token_recuperacao'],$u['token_expira']);
        $_SESSION['usuario']=$u; $_SESSION['usuario_id']=(int)$u['id'];
    }
}
function usuarioAtual(): array { return $_SESSION['usuario'] ?? []; }

// ============================================================
// Dispositivos e sessões
// ============================================================
function registrarDispositivo(PDO $pdo, int $usuarioId, ?string $nome = null): int
{
    $uuid = deviceUuid();
    $ua = substr((string)($_SERVER['HTTP_USER_AGENT'] ?? 'Desconhecido'),0,1000);
    $plataforma = detectarPlataforma($ua);
    $navegador = detectarNavegador($ua);
    $nome = $nome ?: sanitize((string)($_SERVER['HTTP_X_DEVICE_NAME'] ?? ''));
    $nome = $nome ?: trim($plataforma . ' • ' . $navegador);
    $st = $pdo->prepare('SELECT id FROM af_dispositivos WHERE usuario_id=? AND device_uuid=? LIMIT 1');
    $st->execute([$usuarioId,$uuid]);
    $id = (int)($st->fetchColumn() ?: 0);
    if ($id) {
        $pdo->prepare('UPDATE af_dispositivos SET nome=?,plataforma=?,navegador=?,user_agent=?,ip_address=?,ultimo_acesso=NOW(),ativo=1 WHERE id=?')
            ->execute([$nome,$plataforma,$navegador,$ua,clientIp(),$id]);
        return $id;
    }
    $st = $pdo->prepare('INSERT INTO af_dispositivos (usuario_id,device_uuid,nome,plataforma,navegador,user_agent,ip_address,ultimo_acesso,ativo) VALUES (?,?,?,?,?,?,?,NOW(),1)');
    $st->execute([$usuarioId,$uuid,$nome,$plataforma,$navegador,$ua,clientIp()]);
    return (int)$pdo->lastInsertId();
}

function criarSessaoAutenticada(PDO $pdo, array $usuario, bool $lembrar = true): string
{
    session_regenerate_id(true);
    $deviceId = registrarDispositivo($pdo,(int)$usuario['id']);
    $rawToken = bin2hex(random_bytes(32));
    $hash = hash('sha256',$rawToken);
    $dias = $lembrar ? AUTH_SESSION_DAYS : 1;
    $exp = (new DateTimeImmutable('+' . $dias . ' days'))->format('Y-m-d H:i:s');
    $st = $pdo->prepare('INSERT INTO af_sessoes (usuario_id,dispositivo_id,token_hash,php_session_id,ip_address,user_agent,risco,ip_hash,expira_em,criada_em,ultima_atividade) VALUES (?,?,?,?,?,?,0,?,?,NOW(),NOW())');
    $st->execute([(int)$usuario['id'],$deviceId,$hash,session_id(),clientIp(),substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''),0,1000),afIpHash(),$exp]);
    $sid = (int)$pdo->lastInsertId();
    setcookie(AUTH_COOKIE_NAME,$rawToken,cookieOptions(time()+($dias*86400)));
    $_COOKIE[AUTH_COOKIE_NAME]=$rawToken;
    unset($usuario['senha'],$usuario['token_recuperacao'],$usuario['token_expira']);
    $_SESSION['usuario']=$usuario;
    $_SESSION['usuario_id']=(int)$usuario['id'];
    $_SESSION['auth_session_id']=$sid;
    $_SESSION['device_id']=$deviceId;
    $_SESSION['ultima_validacao_auth']=time();
    $pdo->prepare('UPDATE af_usuarios SET ultimo_login=NOW(),atualizado_em=NOW() WHERE id=?')->execute([(int)$usuario['id']]);
    return $rawToken;
}

function validarSessaoAtual(PDO $pdo): bool
{
    if (empty($_SESSION['usuario_id']) || empty($_SESSION['auth_session_id'])) return false;
    if (!empty($_SESSION['ultima_validacao_auth']) && time()-(int)$_SESSION['ultima_validacao_auth']<60) return true;
    $raw=(string)($_COOKIE[AUTH_COOKIE_NAME] ?? '');
    if ($raw==='') return false;
    $st=$pdo->prepare('SELECT s.*,d.device_uuid,u.status,u.seguranca_status,u.suspenso_ate FROM af_sessoes s JOIN af_dispositivos d ON d.id=s.dispositivo_id JOIN af_usuarios u ON u.id=s.usuario_id WHERE s.id=? AND s.usuario_id=? AND s.revogada_em IS NULL AND s.expira_em>NOW() LIMIT 1');
    $st->execute([(int)$_SESSION['auth_session_id'],(int)$_SESSION['usuario_id']]);
    $s=$st->fetch();
    if (!$s || (int)$s['status']!==1 || ($s['seguranca_status']??'normal')==='banido' || (!empty($s['suspenso_ate']) && strtotime((string)$s['suspenso_ate'])>time()) || !hash_equals($s['token_hash'],hash('sha256',$raw)) || !hash_equals($s['device_uuid'],deviceUuid())) return false;
    $pdo->prepare('UPDATE af_sessoes SET ultima_atividade=NOW(),ip_address=? WHERE id=?')->execute([clientIp(),(int)$s['id']]);
    $pdo->prepare('UPDATE af_dispositivos SET ultimo_acesso=NOW(),ip_address=? WHERE id=?')->execute([clientIp(),(int)$s['dispositivo_id']]);
    $_SESSION['ultima_validacao_auth']=time();
    return true;
}

function isLoggedIn(): bool
{
    if (empty($_SESSION['usuario_id'])) return false;
    global $pdo;
    return isset($pdo) ? validarSessaoAtual($pdo) : true;
}
function requireLogin(string $redirect='/auth/login.php'): void
{
    if (!isLoggedIn()) {
        limparSessaoLocal();
        header('Location: '.SITE_URL.$redirect); exit;
    }
    global $pdo;
    if (isset($pdo) && function_exists('afEnforceAgeCompletion')) {
        afEnforceAgeCompletion($pdo, (int)($_SESSION['usuario_id'] ?? 0));
    }
    if (isset($pdo) && function_exists('afEnforceLegalAcceptance')) {
        afEnforceLegalAcceptance($pdo, (int)($_SESSION['usuario_id'] ?? 0));
    }
}
function redirectIfLogged(string $destination='/pages/home.php'): void
{
    if (isLoggedIn()) { header('Location: '.SITE_URL.$destination); exit; }
}
function limparSessaoLocal(): void
{
    setcookie(AUTH_COOKIE_NAME,'',cookieOptions(time()-3600));
    unset($_COOKIE[AUTH_COOKIE_NAME]);
    $_SESSION=[];
    if (session_status()===PHP_SESSION_ACTIVE) session_regenerate_id(true);
}
function encerrarSessaoAtual(PDO $pdo): void
{
    if (!empty($_SESSION['auth_session_id'])) $pdo->prepare('UPDATE af_sessoes SET revogada_em=NOW() WHERE id=?')->execute([(int)$_SESSION['auth_session_id']]);
    limparSessaoLocal();
}
function encerrarOutrasSessoes(PDO $pdo,int $uid,int $current): int
{
    $st=$pdo->prepare('UPDATE af_sessoes SET revogada_em=NOW() WHERE usuario_id=? AND id<>? AND revogada_em IS NULL');
    $st->execute([$uid,$current]); return $st->rowCount();
}

// ============================================================
// Recuperação de senha e e-mail
// ============================================================
function enviarEmailSimples(string $para,string $assunto,string $html): bool
{
    $headers=[
        'MIME-Version: 1.0','Content-type: text/html; charset=UTF-8',
        'From: '.MAIL_FROM_NAME.' <'.MAIL_FROM.'>'
    ];
    $ok=@mail($para,$assunto,$html,implode("\r\n",$headers));
    if (!$ok || APP_ENV!=='production') {
        if (!is_dir(STORAGE_PATH.'logs')) @mkdir(STORAGE_PATH.'logs',0755,true);
        file_put_contents(STORAGE_PATH.'logs/mail.log',"\n--- ".date('c')." ---\nPARA: $para\nASSUNTO: $assunto\n$html\n",FILE_APPEND);
    }
    return $ok;
}
function criarTokenRecuperacao(PDO $pdo,int $uid): string
{
    $raw=bin2hex(random_bytes(32));
    $hash=hash('sha256',$raw);
    $pdo->prepare('UPDATE af_password_resets SET usado_em=NOW() WHERE usuario_id=? AND usado_em IS NULL')->execute([$uid]);
    $pdo->prepare('INSERT INTO af_password_resets (usuario_id,token_hash,ip_address,expira_em,criado_em) VALUES (?,?,?,DATE_ADD(NOW(),INTERVAL 60 MINUTE),NOW())')->execute([$uid,$hash,clientIp()]);
    return $raw;
}
function validarTokenRecuperacao(PDO $pdo,string $raw): ?array
{
    if (!preg_match('/^[a-f0-9]{64}$/',$raw)) return null;
    $st=$pdo->prepare('SELECT r.*,u.email,u.nome FROM af_password_resets r JOIN af_usuarios u ON u.id=r.usuario_id WHERE r.token_hash=? AND r.usado_em IS NULL AND r.expira_em>NOW() LIMIT 1');
    $st->execute([hash('sha256',$raw)]); return $st->fetch() ?: null;
}

// ============================================================
// Notificações
// ============================================================
function criarNotificacao(PDO $pdo,int $uid,string $tipo,string $titulo,string $mensagem,array $dados=[]): void
{
    $st=$pdo->prepare('INSERT INTO af_notificacoes (usuario_id,tipo,titulo,mensagem,dados,criada_em) VALUES (?,?,?,?,?,NOW())');
    $st->execute([$uid,$tipo,$titulo,$mensagem,$dados?json_encode($dados,JSON_UNESCAPED_UNICODE):null]);
}
function contarNotificacoesNaoLidas(PDO $pdo,int $uid): int
{
    $st=$pdo->prepare('SELECT COUNT(*) FROM af_notificacoes WHERE usuario_id=? AND lida_em IS NULL');$st->execute([$uid]);return (int)$st->fetchColumn();
}

// ============================================================
// Upload de avatar
// ============================================================
function processarAvatar(array $file,string $prefixo=''): ?string
{
    if (($file['error']??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_OK || ($file['size']??0)>MAX_UPLOAD_SIZE) return null;
    $tmp=(string)($file['tmp_name']??'');
    if ($tmp==='' || !is_file($tmp)) return null;

    $mime=null;
    if (class_exists('finfo')) {
        try {
            $finfo=new finfo(FILEINFO_MIME_TYPE);
            $detected=$finfo->file($tmp);
            if (is_string($detected) && $detected!=='') $mime=$detected;
        } catch (Throwable $e) {
            error_log('[AURA-AVATAR] Falha no Fileinfo: '.$e->getMessage());
        }
    }
    if (!$mime && function_exists('mime_content_type')) {
        $detected=@mime_content_type($tmp);
        if (is_string($detected) && $detected!=='') $mime=$detected;
    }
    if (!$mime && function_exists('getimagesize')) {
        $imageInfo=@getimagesize($tmp);
        if (is_array($imageInfo) && !empty($imageInfo['mime'])) $mime=(string)$imageInfo['mime'];
    }

    if (!is_string($mime) || !in_array($mime,ALLOWED_MIMES,true)) return null;
    $ext=['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp'][$mime] ?? null;
    if (!$ext) return null;
    if (!is_dir(UPLOAD_PATH) && !mkdir(UPLOAD_PATH,0755,true) && !is_dir(UPLOAD_PATH)) return null;
    $name=($prefixo?:'avatar').'_'.bin2hex(random_bytes(10)).'.'.$ext;
    return move_uploaded_file($tmp,UPLOAD_PATH.$name) ? $name : null;
}
function urlAvatar(?string $foto,?string $externa=null): string
{
    if ($foto && is_file(UPLOAD_PATH.$foto)) return UPLOAD_URL.rawurlencode($foto);
    if ($externa && filter_var($externa,FILTER_VALIDATE_URL)) return $externa;
    return DEFAULT_AVATAR;
}

// ============================================================
// Nível, ranking e formatação
// ============================================================
/**
 * Curva de XP revisada — Diamantes Parte 07.
 *
 * A progressão ficou mais difícil para evitar que vídeo, partida ou
 * interação simples subam muitos níveis rapidamente.
 * Os valores abaixo representam o XP TOTAL necessário para alcançar o nível.
 */
function afXpTotalParaNivel(int $nivel): int
{
    static $base = [
        1 => 0,
        2 => 150,
        3 => 400,
        4 => 800,
        5 => 1400,
        6 => 2300,
        7 => 3600,
        8 => 5400,
        9 => 7800,
        10 => 11000,
    ];

    $nivel = max(1, $nivel);
    if (isset($base[$nivel])) return $base[$nivel];

    $total = $base[10];
    for ($n = 10; $n < $nivel; $n++) {
        $total += (int)ceil(100 * ($n ** 2.15));
    }
    return $total;
}

function calcularNivel(int $xp): int
{
    $xp = max(0, $xp);
    $nivel = 1;
    // trava de segurança para evitar loop eterno em XP muito alto
    while ($nivel < 200 && afXpTotalParaNivel($nivel + 1) <= $xp) {
        $nivel++;
    }
    return $nivel;
}

function xpParaProximoNivel(int $nivel): int
{
    $nivel = max(1, $nivel);
    return max(1, afXpTotalParaNivel($nivel + 1) - afXpTotalParaNivel($nivel));
}

function xpNoNivelAtual(int $xp, int $nivel): int
{
    $xp = max(0, $xp);
    $nivel = max(1, $nivel);
    return max(0, $xp - afXpTotalParaNivel($nivel));
}
function getTitulo(int $nivel): string { $t='Aura Initiate';foreach(TITULOS as $min=>$nome)if($nivel>=$min)$t=$nome;return $t; }
function getCorTitulo(string $titulo): string { return match($titulo){'Aura Farmer'=>'#00d4ff','Aura Master'=>'#7b2fff','Aura Legend'=>'#ffd700','Aura Overlord'=>'#ff4081',default=>'#888'}; }
function formatarNumero(float $n): string { if($n>=1_000_000)return number_format($n/1_000_000,1,',','.').'M';if($n>=1000)return number_format($n/1000,1,',','.').'K';return number_format($n,0,',','.'); }
function formatarPontuacao(float $n): string { return number_format($n,2,',','.'); }
function posicaoNoRanking(PDO $pdo,int $uid): int { if(function_exists('afCalcularPosicaoPeriodo'))return afCalcularPosicaoPeriodo($pdo,$uid,'global');$st=$pdo->prepare('SELECT COUNT(*)+1 FROM af_usuarios WHERE melhor_pontuacao>(SELECT melhor_pontuacao FROM af_usuarios WHERE id=?) AND status=1');$st->execute([$uid]);return (int)($st->fetchColumn()?:1); }

// ============================================================
// Flash
// ============================================================
function setFlash(string $tipo,string $msg): void { $_SESSION['flash']=['tipo'=>$tipo,'msg'=>$msg]; }
function getFlash(): ?array { if(!isset($_SESSION['flash']))return null;$f=$_SESSION['flash'];unset($_SESSION['flash']);return $f; }
function renderFlash(): string { $f=getFlash();if(!$f)return '';$t=$f['tipo']==='success'?'success':'error';return '<div class="flash flash--'.$t.'"><span>'.($t==='success'?'✓':'✕').'</span> '.esc($f['msg']).'</div>'; }

// ============================================================
// Conquistas
// ============================================================
function verificarConquistas(PDO $pdo,array $u): void
{
    $uid=(int)$u['id'];
    $nivel=calcularNivel((int)($u['xp_total']??0));
    $extras=['sequencia'=>0,'compartilhamentos'=>0,'seguidores'=>0,'aura_maxima'=>0,'ranking_top1'=>0];
    try{
        $st=$pdo->prepare('SELECT sequencia_atual FROM af_progressao_usuario WHERE usuario_id=?');$st->execute([$uid]);$extras['sequencia']=(int)($st->fetchColumn()?:0);
        $st=$pdo->prepare('SELECT COUNT(*) FROM af_compartilhamentos WHERE usuario_id=?');$st->execute([$uid]);$extras['compartilhamentos']=(int)$st->fetchColumn();
        $st=$pdo->prepare('SELECT COUNT(*) FROM af_seguidores WHERE seguido_id=?');$st->execute([$uid]);$extras['seguidores']=(int)$st->fetchColumn();
        $st=$pdo->prepare("SELECT COUNT(*) FROM af_partidas WHERE usuario_id=? AND status='finalizada' AND controlled_event_id IS NULL AND aura_gerada>=1000");$st->execute([$uid]);$extras['aura_maxima']=(int)$st->fetchColumn();
        $pos=posicaoNoRanking($pdo,$uid);$total=(int)$pdo->query('SELECT COUNT(*) FROM af_usuarios WHERE status=1')->fetchColumn();
        $extras['ranking_top1']=($total>0 && $pos<=max(1,(int)ceil($total*.01)))?1:0;
    }catch(Throwable $e){/* compatibilidade durante migração */}

    $novas=[];$xpExtra=0;
    $rows=$pdo->query('SELECT * FROM af_conquistas WHERE COALESCE(ativo,1)=1 ORDER BY id')->fetchAll();
    foreach($rows as $c){
        $st=$pdo->prepare('SELECT id FROM af_usuario_conquistas WHERE usuario_id=? AND conquista_id=?');$st->execute([$uid,$c['id']]);if($st->fetchColumn())continue;
        $valor=match($c['criterio']){
            'total_partidas'=>(int)($u['total_partidas']??0),
            'total_vitorias'=>(int)($u['total_vitorias']??0),
            'aura_total'=>(int)($u['aura_total']??0),
            'total_likes'=>(int)($u['total_likes']??0),
            'total_superlikes'=>(int)($u['total_superlikes']??0),
            'melhor_pontuacao'=>(int)floor((float)($u['melhor_pontuacao']??0)),
            'nivel'=>$nivel,
            'sequencia'=>$extras['sequencia'],
            'compartilhamentos'=>$extras['compartilhamentos'],
            'seguidores'=>$extras['seguidores'],
            'aura_maxima'=>$extras['aura_maxima'],
            'ranking_top1'=>$extras['ranking_top1'],
            default=>0,
        };
        if($valor<(int)$c['valor_criterio'])continue;
        $ins=$pdo->prepare('INSERT IGNORE INTO af_usuario_conquistas(usuario_id,conquista_id,desbloqueada_em) VALUES (?,?,NOW())');$ins->execute([$uid,$c['id']]);
        if($ins->rowCount()>0){
            $xp=(int)($c['xp_recompensa']??0);$xpExtra+=$xp;$novas[]=$c;
            criarNotificacao($pdo,$uid,'achievement','Conquista desbloqueada: '.$c['nome'],$c['descricao'].($xp>0?' +'.$xp.' XP.':''),['conquista_id'=>(int)$c['id']]);
            if($xp>0 && function_exists('afRegistrarXp'))afRegistrarXp($pdo,$uid,$xp,'conquista','Conquista: '.$c['nome']);
        }
    }
    if($xpExtra>0){
        $st=$pdo->prepare('SELECT xp_total FROM af_usuarios WHERE id=?');$st->execute([$uid]);$novoXp=(int)$st->fetchColumn()+$xpExtra;$novoNivel=calcularNivel($novoXp);
        $pdo->prepare('UPDATE af_usuarios SET xp_total=?,nivel=?,titulo=?,atualizado_em=NOW() WHERE id=?')->execute([$novoXp,$novoNivel,getTitulo($novoNivel),$uid]);
        if($novoNivel>$nivel)criarNotificacao($pdo,$uid,'level_up','Nível '.$novoNivel.' alcançado!','O XP das conquistas elevou sua conta para um novo nível.',['nivel'=>$novoNivel]);
        if(function_exists('afSincronizarRecompensas'))afSincronizarRecompensas($pdo,$uid,$novoNivel);
    }
}

// ============================================================
// API JSON / Bearer Token
// ============================================================
function jsonResponse(array $data,int $status=200): never
{
    http_response_code($status);header('Content-Type: application/json; charset=UTF-8');echo json_encode($data,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);exit;
}
function requestJson(): array
{
    $raw=file_get_contents('php://input');$data=json_decode($raw?:'{}',true);return is_array($data)?$data:[];
}
function bearerToken(): string
{
    $h=(string)($_SERVER['HTTP_AUTHORIZATION']??'');return preg_match('/^Bearer\s+(.+)$/i',$h,$m)?trim($m[1]):'';
}
function apiUsuario(PDO $pdo): array
{
    $raw=bearerToken();if(!$raw)jsonResponse(['ok'=>false,'erro'=>'Token ausente.'],401);
    $st=$pdo->prepare("SELECT s.id sessao_id,s.dispositivo_id,u.*,p.bio,p.cidade,p.estado,p.pais,a.data_nascimento,a.pais_residencia,a.tipo_conta,a.status_idade,a.idade_declarada FROM af_sessoes s JOIN af_usuarios u ON u.id=s.usuario_id LEFT JOIN af_perfis p ON p.usuario_id=u.id LEFT JOIN af_user_age_profiles a ON a.usuario_id=u.id WHERE s.token_hash=? AND s.revogada_em IS NULL AND s.expira_em>NOW() AND u.status=1 AND u.seguranca_status<>'banido' AND (u.suspenso_ate IS NULL OR u.suspenso_ate<=NOW()) LIMIT 1");
    $st->execute([hash('sha256',$raw)]);$u=$st->fetch();if(!$u)jsonResponse(['ok'=>false,'erro'=>'Sessão inválida ou expirada.'],401);
    $pdo->prepare('UPDATE af_sessoes SET ultima_atividade=NOW() WHERE id=?')->execute([$u['sessao_id']]);unset($u['senha'],$u['token_recuperacao'],$u['token_expira']);if(function_exists('afStage24RegisterApiContext'))afStage24RegisterApiContext((int)$u['id'],null,true,null,['auth'=>'user']);if(function_exists('afAssertApiAgeAccess'))afAssertApiAgeAccess($pdo,$u);if(function_exists('afAssertApiLegalAccess'))afAssertApiLegalAccess($pdo,$u);return $u;
}

require_once __DIR__ . '/age_functions.php';
require_once __DIR__ . '/age_assurance_functions.php';
require_once __DIR__ . '/guardian_functions.php';
require_once __DIR__ . '/guardian_verification_functions.php';
require_once __DIR__ . '/consent_functions.php';
require_once __DIR__ . '/controlled_event_functions.php';
$guardianDashboardModule=__DIR__ . '/guardian_dashboard_functions.php';
if (is_file($guardianDashboardModule)) {
    require_once $guardianDashboardModule;
} else {
    error_log('[AURA-MODULE] Arquivo guardian_dashboard_functions.php ausente. Recursos parentais foram desativados até a instalação ser concluída.');
}
require_once __DIR__ . '/minor_policy_functions.php';
require_once __DIR__ . '/safe_camera_functions.php';
require_once __DIR__ . '/privacy_functions.php';
require_once __DIR__ . '/privacy_deletion_functions.php';
require_once __DIR__ . '/security_functions.php';
require_once __DIR__ . '/child_safety_completion_functions.php';

require_once __DIR__ . '/moderation_functions.php';
require_once __DIR__ . '/stage24_audit_functions.php';

$diamondModule = __DIR__ . '/diamond_functions.php';
if (is_file($diamondModule)) {
    require_once $diamondModule;
}

require_once __DIR__ . '/stage25_release_functions.php';
