<?php
/**
 * AURA FARMING — Etapa 12
 * Segurança, rate limit, trilha de auditoria, prova de presença e antifraude.
 */

function afPrivacyHash(string $value): string
{
    return hash_hmac('sha256', trim($value), SECURITY_HASH_SALT);
}

function afIpHash(): string
{
    return afPrivacyHash(clientIp());
}

function afFingerprintHash(string $fingerprint): string
{
    $fingerprint = trim(substr($fingerprint, 0, 255));
    return $fingerprint === '' ? '' : afPrivacyHash($fingerprint);
}

function afSeveridadePorPontos(int $pontos): string
{
    return match (true) {
        $pontos >= 30 => 'critica',
        $pontos >= 18 => 'alta',
        $pontos >= 8  => 'media',
        $pontos >= 3  => 'baixa',
        default => 'info',
    };
}

function afLogSeguranca(
    PDO $pdo,
    string $categoria,
    int $pontos = 0,
    array $detalhes = [],
    ?int $usuarioId = null,
    ?int $partidaId = null,
    ?int $dispositivoId = null,
    string $fingerprintHash = ''
): void {
    $pontos = max(0, min(100, $pontos));
    $st = $pdo->prepare("INSERT INTO af_eventos_seguranca
        (usuario_id,partida_id,dispositivo_id,categoria,severidade,pontos,ip_hash,fingerprint_hash,detalhes,status,criado_em)
        VALUES (?,?,?,?,?,?,?,?,?,'aberto',NOW())");
    $st->execute([
        $usuarioId ?: null,
        $partidaId ?: null,
        $dispositivoId ?: null,
        substr($categoria, 0, 60),
        afSeveridadePorPontos($pontos),
        $pontos,
        afIpHash(),
        $fingerprintHash ?: null,
        $detalhes ? json_encode($detalhes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
    ]);
}

function afRateLimit(PDO $pdo, string $bucket, string $identifier, int $max, int $windowSeconds, int $blockSeconds = 0): array
{
    $max = max(1, $max);
    $windowSeconds = max(1, $windowSeconds);
    $blockSeconds = max($windowSeconds, $blockSeconds ?: $windowSeconds);
    $bucket = substr(preg_replace('/[^a-z0-9_\-]/i', '', $bucket), 0, 60) ?: 'generic';
    $keyHash = afPrivacyHash($bucket . '|' . $identifier);

    $pdo->beginTransaction();
    try {
        $st = $pdo->prepare('SELECT * FROM af_rate_limits WHERE chave_hash=? FOR UPDATE');
        $st->execute([$keyHash]);
        $row = $st->fetch();
        $now = time();
        if (!$row) {
            $pdo->prepare('INSERT INTO af_rate_limits(chave_hash,bucket,contador,janela_inicio,bloqueado_ate,atualizado_em) VALUES (?,?,1,NOW(),NULL,NOW())')
                ->execute([$keyHash,$bucket]);
            $pdo->commit();
            return ['allowed'=>true,'remaining'=>$max-1,'retry_after'=>0,'count'=>1];
        }

        $blockedUntil = !empty($row['bloqueado_ate']) ? strtotime((string)$row['bloqueado_ate']) : 0;
        if ($blockedUntil > $now) {
            $pdo->commit();
            return ['allowed'=>false,'remaining'=>0,'retry_after'=>$blockedUntil-$now,'count'=>(int)$row['contador']];
        }

        $windowStart = strtotime((string)$row['janela_inicio']);
        if (!$windowStart || $now - $windowStart >= $windowSeconds) {
            $pdo->prepare('UPDATE af_rate_limits SET contador=1,janela_inicio=NOW(),bloqueado_ate=NULL,atualizado_em=NOW() WHERE chave_hash=?')->execute([$keyHash]);
            $pdo->commit();
            return ['allowed'=>true,'remaining'=>$max-1,'retry_after'=>0,'count'=>1];
        }

        $count = (int)$row['contador'] + 1;
        if ($count > $max) {
            $pdo->prepare('UPDATE af_rate_limits SET contador=?,bloqueado_ate=DATE_ADD(NOW(),INTERVAL ? SECOND),atualizado_em=NOW() WHERE chave_hash=?')
                ->execute([$count,$blockSeconds,$keyHash]);
            $pdo->commit();
            return ['allowed'=>false,'remaining'=>0,'retry_after'=>$blockSeconds,'count'=>$count];
        }

        $pdo->prepare('UPDATE af_rate_limits SET contador=?,atualizado_em=NOW() WHERE chave_hash=?')->execute([$count,$keyHash]);
        $pdo->commit();
        return ['allowed'=>true,'remaining'=>max(0,$max-$count),'retry_after'=>0,'count'=>$count];
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        // Falha aberta apenas para não derrubar o aplicativo por problema auxiliar.
        error_log('[AURA-RATE] '.$e->getMessage());
        return ['allowed'=>true,'remaining'=>0,'retry_after'=>0,'count'=>0];
    }
}

function afRateLimitOrFail(PDO $pdo, string $bucket, string $identifier, int $max, int $windowSeconds, int $blockSeconds = 0): void
{
    $r = afRateLimit($pdo,$bucket,$identifier,$max,$windowSeconds,$blockSeconds);
    if (!$r['allowed']) {
        throw new RuntimeException('Muitas tentativas. Aguarde aproximadamente '.max(1,(int)ceil($r['retry_after']/60)).' minuto(s).');
    }
}

function afResetRateLimit(PDO $pdo, string $bucket, string $identifier): void
{
    $keyHash = afPrivacyHash($bucket . '|' . $identifier);
    $pdo->prepare('DELETE FROM af_rate_limits WHERE chave_hash=?')->execute([$keyHash]);
}

function afRegistrarTentativaLogin(PDO $pdo, string $identificador, bool $sucesso, ?int $usuarioId = null, string $motivo = ''): void
{
    $pdo->prepare('INSERT INTO af_tentativas_login(usuario_id,identificador_hash,ip_hash,sucesso,motivo,user_agent_hash,criado_em) VALUES (?,?,?,?,?,?,NOW())')
        ->execute([
            $usuarioId ?: null,
            afPrivacyHash(strtolower(trim($identificador))),
            afIpHash(),
            $sucesso ? 1 : 0,
            substr($motivo,0,120) ?: null,
            afPrivacyHash(substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''),0,1000)),
        ]);
}

function afIsAdmin(?array $usuario = null): bool
{
    $usuario = $usuario ?: usuarioAtual();
    if (in_array((string)($usuario['papel'] ?? ''), ['admin','moderador'], true)) return true;
    $email = strtolower((string)($usuario['email'] ?? ''));
    foreach (ADMIN_EMAILS as $adminEmail) {
        if ($email !== '' && hash_equals(strtolower($adminEmail), $email)) return true;
    }
    return false;
}

function afRequireAdmin(): void
{
    requireLogin();
    if (!afIsAdmin()) {
        http_response_code(403);
        exit('Acesso restrito à equipe de segurança.');
    }
}

function afEstadoSegurancaUsuario(PDO $pdo, int $usuarioId): array
{
    $st = $pdo->prepare('SELECT id,papel,seguranca_status,risco_conta,suspenso_ate,status FROM af_usuarios WHERE id=? LIMIT 1');
    $st->execute([$usuarioId]);
    return $st->fetch() ?: [];
}

function afAssertContaPermitida(PDO $pdo, int $usuarioId, bool $paraPartida = false): array
{
    $u = afEstadoSegurancaUsuario($pdo,$usuarioId);
    if (!$u || (int)$u['status'] !== 1 || ($u['seguranca_status'] ?? '') === 'banido') {
        throw new RuntimeException('Esta conta está indisponível por segurança.');
    }
    if (!empty($u['suspenso_ate']) && strtotime((string)$u['suspenso_ate']) > time()) {
        throw new RuntimeException('A conta está temporariamente suspensa até '.date('d/m/Y H:i',strtotime((string)$u['suspenso_ate'])).'.');
    }
    if ($paraPartida && ($u['seguranca_status'] ?? '') === 'restrito') {
        throw new RuntimeException('Novas partidas estão temporariamente bloqueadas para esta conta.');
    }
    return $u;
}

function afRegistrarVinculoFingerprint(PDO $pdo, int $usuarioId, int $dispositivoId, string $fingerprint): array
{
    $hash = afFingerprintHash($fingerprint);
    if ($hash === '') return ['hash'=>'','accounts'=>1,'risk'=>0];
    $pdo->prepare("INSERT INTO af_dispositivo_contas(fingerprint_hash,usuario_id,dispositivo_id,primeira_vez_em,ultima_vez_em,partidas)
                   VALUES (?,?,?,NOW(),NOW(),1)
                   ON DUPLICATE KEY UPDATE dispositivo_id=VALUES(dispositivo_id),ultima_vez_em=NOW(),partidas=partidas+1")
        ->execute([$hash,$usuarioId,$dispositivoId ?: null]);
    $st=$pdo->prepare('SELECT COUNT(DISTINCT usuario_id) FROM af_dispositivo_contas WHERE fingerprint_hash=? AND ultima_vez_em>=DATE_SUB(NOW(),INTERVAL 90 DAY)');
    $st->execute([$hash]);
    $accounts=(int)$st->fetchColumn();
    $risk = match (true) { $accounts >= 6 => 35, $accounts >= 4 => 20, $accounts >= 3 => 10, default => 0 };
    $pdo->prepare('UPDATE af_dispositivos SET fingerprint_hash=?,contas_vinculadas=?,risco=GREATEST(risco,?),ultima_revisao_em=NOW() WHERE id=?')
        ->execute([$hash,$accounts,$risk,$dispositivoId]);
    if ($risk > 0) {
        afLogSeguranca($pdo,'multiplas_contas_dispositivo',$risk,['contas'=>$accounts],$usuarioId,null,$dispositivoId,$hash);
    }
    return ['hash'=>$hash,'accounts'=>$accounts,'risk'=>$risk];
}

function afLivenessCatalogo(): array
{
    return [
        'hands_up' => ['label'=>'LEVANTE AS DUAS MÃOS','moves'=>['power_pose']],
        'arms_open' => ['label'=>'ABRA OS BRAÇOS','moves'=>['aura_wings']],
        'cross_guard' => ['label'=>'CRUZE OS BRAÇOS','moves'=>['cross_guard']],
        'squat' => ['label'=>'FAÇA UM AGACHAMENTO','moves'=>['aura_squat']],
        'energy_hands' => ['label'=>'UNA AS MÃOS À FRENTE','moves'=>['energy_hands']],
    ];
}

function afEmitirLiveness(PDO $pdo, array $p, int $decorrido): ?array
{
    // Modo de apresentação livre: não interrompe a câmera com comandos obrigatórios.
    // Limpa desafios antigos que possam ter ficado ativos antes desta atualização.
    if (!empty($p['liveness_challenge_code']) || !empty($p['liveness_challenge_nonce']) || !empty($p['liveness_challenge_expira'])) {
        $pdo->prepare('UPDATE af_partidas SET liveness_challenge_code=NULL,liveness_challenge_nonce=NULL,liveness_challenge_expira=NULL WHERE id=?')
            ->execute([(int)$p['id']]);
    }
    return null;
}

function afProcessarLiveness(PDO $pdo, array $p, array $visao): array
{
    // A validação antifraude continua usando tempo, dispositivo, amostras e métricas,
    // porém sem solicitar movimentos específicos ao jogador.
    return ['passed'=>true,'risk'=>0,'reason'=>''];
}

function afAnalisarAmostraAntifraude(PDO $pdo, array $p, int $tempoCliente, int $decorridoServidor, array $visao, float $intensidade): array
{
    $risk=0;$reasons=[];
    $sampleData=[
        round($intensidade,1),
        round((float)($visao['confianca']??0),1),round((float)($visao['cobertura']??0),1),
        round((float)($visao['precisao']??0),1),round((float)($visao['fluidez']??0),1),
        round((float)($visao['originalidade']??0),1),round((float)($visao['dificuldade']??0),1),
        (string)($visao['movimento']??''),(int)($visao['frames_pose']??0),(int)($visao['frames_maos']??0),
    ];
    $signature=hash('sha256',json_encode($sampleData));
    $duplicates=(int)($p['amostras_duplicadas']??0);
    if (!empty($p['ultima_assinatura_amostra']) && hash_equals((string)$p['ultima_assinatura_amostra'],$signature)) {
        $duplicates++;
        if ($duplicates >= 3) {$risk+=4;$reasons[]='amostras_idênticas';}
        if ($duplicates >= 8) {$risk+=8;$reasons[]='padrão_repetitivo_extremo';}
    }

    $lastClient=(int)($p['ultimo_tempo_cliente']??0);
    $interval=$lastClient>0 ? $tempoCliente-$lastClient : 0;
    $mean=(float)($p['intervalo_amostra_medio']??0);
    $m2=(float)($p['intervalo_amostra_m2']??0);
    $n=max(0,(int)($p['total_amostras']??0));
    if ($lastClient>0) {
        if ($interval<=0) {$risk+=18;$reasons[]='relogio_cliente_retrocedeu';}
        elseif ($interval>7) {$risk+=4;$reasons[]='lacuna_de_amostras';}
        $delta=$interval-$mean;$mean += $delta/max(1,$n);$m2 += $delta*($interval-$mean);
    }
    if (abs($tempoCliente-$decorridoServidor)>8) {$risk+=5;$reasons[]='dessincronia_temporal';}
    $metrics=[(float)($visao['precisao']??0),(float)($visao['fluidez']??0),(float)($visao['originalidade']??0),(float)($visao['dificuldade']??0)];
    if (min($metrics)>97.5) {$risk+=5;$reasons[]='metricas_improvavelmente_perfeitas';}
    if (($visao['modo']??'')==='movimento' && $intensidade>92) {$risk+=3;$reasons[]='fallback_intensidade_extrema';}

    $live=afProcessarLiveness($pdo,$p,$visao);
    $risk += (int)$live['risk']; if ($live['reason']) $reasons[]=$live['reason'];

    $newScore=min(100,(int)($p['antifraude_score']??0)+$risk);
    $replayScore=min(100,max((float)($p['score_padrao_replay']??0),$duplicates*8 + ($risk>0?5:0)));
    $pdo->prepare('UPDATE af_partidas SET antifraude_score=?,antifraude_motivos=?,ultimo_tempo_cliente=?,intervalo_amostra_medio=?,intervalo_amostra_m2=?,amostras_duplicadas=?,ultima_assinatura_amostra=?,score_padrao_replay=? WHERE id=?')
        ->execute([$newScore,json_encode(array_values(array_unique(array_merge(json_decode((string)($p['antifraude_motivos']??'[]'),true)?:[],$reasons))),JSON_UNESCAPED_UNICODE),$tempoCliente,$mean,$m2,$duplicates,$signature,$replayScore,(int)$p['id']]);
    if ($risk>=8) afLogSeguranca($pdo,'amostra_suspeita',$risk,['motivos'=>$reasons,'tempo_cliente'=>$tempoCliente,'tempo_servidor'=>$decorridoServidor],(int)$p['usuario_id'],(int)$p['id'],(int)($p['dispositivo_id']??0),afFingerprintHash((string)($p['device_fingerprint']??'')));
    return ['risk'=>$risk,'score'=>$newScore,'reasons'=>$reasons,'signature'=>$signature,'duplicates'=>$duplicates,'mean'=>$mean,'m2'=>$m2,'liveness'=>$live];
}

function afAvaliarPartidaFinal(PDO $pdo, array $p): array
{
    $score=(int)($p['antifraude_score']??0);$reasons=json_decode((string)($p['antifraude_motivos']??'[]'),true)?:[];
    $samples=(int)($p['total_amostras']??0);$duplicates=(int)($p['amostras_duplicadas']??0);
    if ($samples<25) {$score+=15;$reasons[]='poucas_amostras';}
    if ($samples>0 && $duplicates/$samples>.25) {$score+=18;$reasons[]='alta_taxa_de_duplicacao';}
    $n=max(1,$samples-1);$variance=((float)($p['intervalo_amostra_m2']??0))/$n;
    if ($samples>20 && $variance<.005) {$score+=8;$reasons[]='intervalos_robotizados';}
    if ((float)($p['score_padrao_replay']??0)>=55) {$score+=18;$reasons[]='padrao_compativel_com_replay';}
    $score=min(100,$score);
    $status = $score>=ANTIFRAUD_REJECT_SCORE ? 'rejeitada' : ($score>=ANTIFRAUD_REVIEW_SCORE ? 'revisao' : 'aprovada');
    return ['score'=>$score,'status'=>$status,'reasons'=>array_values(array_unique($reasons)),'approved'=>$status==='aprovada'];
}

function afResumoSeguranca(PDO $pdo, int $usuarioId): array
{
    $u=afEstadoSegurancaUsuario($pdo,$usuarioId);
    $st=$pdo->prepare("SELECT COUNT(*) total,
        SUM(antifraude_status='aprovada') aprovadas,
        SUM(antifraude_status='revisao') revisao,
        SUM(antifraude_status='rejeitada') rejeitadas
        FROM af_partidas WHERE usuario_id=? AND status='finalizada'");$st->execute([$usuarioId]);$matches=$st->fetch()?:[];
    $st=$pdo->prepare('SELECT COUNT(*) FROM af_dispositivos WHERE usuario_id=? AND ativo=1');$st->execute([$usuarioId]);$devices=(int)$st->fetchColumn();
    $st=$pdo->prepare('SELECT categoria,severidade,pontos,criado_em,status FROM af_eventos_seguranca WHERE usuario_id=? ORDER BY id DESC LIMIT 10');$st->execute([$usuarioId]);$events=$st->fetchAll()?:[];
    return ['usuario'=>$u,'partidas'=>$matches,'dispositivos'=>$devices,'eventos'=>$events];
}

function afCreditarPartidaAprovadaManualmente(PDO $pdo, array $partida): array
{
    $partidaId = (int)$partida['id'];
    $usuarioId = (int)$partida['usuario_id'];
    $score = round((float)$partida['pontuacao_total'], 2);
    $aura = min((float)AURA_MAX, (float)$partida['aura_gerada']);

    $st = $pdo->prepare('SELECT xp_total,nivel,melhor_pontuacao FROM af_usuarios WHERE id=? FOR UPDATE');
    $st->execute([$usuarioId]);
    $usuario = $st->fetch();
    if (!$usuario) throw new RuntimeException('Usuário da partida não encontrado.');

    $melhorAntes = (float)$usuario['melhor_pontuacao'];
    $novoRecorde = $score > $melhorAntes;
    $nivelAntes = calcularNivel((int)$usuario['xp_total']);
    $xp = afCalcularXpPartida($partida,$score,$novoRecorde,$aura,0);
    $novoXp = (int)$usuario['xp_total'] + (int)$xp['total'];
    $novoNivel = calcularNivel($novoXp);

    $pdo->prepare("UPDATE af_partidas SET antifraude_status='aprovada',pontuacao_validada=?,validada_em=NOW(),xp_ganho=?,xp_base=?,xp_bonus=?,xp_detalhes=?,melhor_pessoal_antes=?,novo_recorde=? WHERE id=?")
        ->execute([$score,(int)$xp['total'],(int)$xp['base'],(int)$xp['bonus'],json_encode($xp['detalhes'],JSON_UNESCAPED_UNICODE),$melhorAntes,$novoRecorde?1:0,$partidaId]);

    // total_partidas já foi contabilizado no encerramento original; aqui entram somente os créditos retidos.
    $pdo->prepare('UPDATE af_usuarios SET aura_total=aura_total+?,total_likes=total_likes+?,total_superlikes=total_superlikes+?,xp_total=?,nivel=?,titulo=?,melhor_pontuacao=GREATEST(melhor_pontuacao,?),atualizado_em=NOW() WHERE id=?')
        ->execute([(int)floor($aura),(int)$partida['likes_recebidos'],(int)$partida['superlikes_recebidos'],$novoXp,$novoNivel,getTitulo($novoNivel),$score,$usuarioId]);

    afRegistrarXp($pdo,$usuarioId,(int)$xp['total'],'partida','XP liberado após revisão antifraude',$partidaId,null,array_merge($xp['detalhes'],['revisao_manual'=>true]));
    $partidaAtualizada = array_merge($partida,['id'=>$partidaId,'pontuacao_validada'=>$score,'antifraude_status'=>'aprovada']);
    afAtualizarMissoesPartida($pdo,$usuarioId,$partidaAtualizada,$score,$aura);
    afSincronizarRecompensas($pdo,$usuarioId,$novoNivel,$partidaId);

    foreach (['global','diario','semanal','mensal'] as $tipo) {
        $st = $pdo->prepare('SELECT id FROM af_ranking WHERE partida_id=? AND tipo=? LIMIT 1');
        $st->execute([$partidaId,$tipo]);
        if (!$st->fetchColumn()) {
            $pdo->prepare('INSERT INTO af_ranking(usuario_id,partida_id,pontuacao,tipo,data_registro) VALUES (?,?,?,?,NOW())')
                ->execute([$usuarioId,$partidaId,$score,$tipo]);
        }
    }

    $posicoes = [
        'diario'=>afCalcularPosicaoPeriodo($pdo,$usuarioId,'diario'),
        'semanal'=>afCalcularPosicaoPeriodo($pdo,$usuarioId,'semanal'),
        'mensal'=>afCalcularPosicaoPeriodo($pdo,$usuarioId,'mensal'),
        'global'=>afCalcularPosicaoPeriodo($pdo,$usuarioId,'global'),
    ];
    $pdo->prepare('UPDATE af_partidas SET ranking_diario_posicao=?,ranking_semanal_posicao=?,ranking_mensal_posicao=?,ranking_global_posicao=? WHERE id=?')
        ->execute([$posicoes['diario'],$posicoes['semanal'],$posicoes['mensal'],$posicoes['global'],$partidaId]);

    $st = $pdo->prepare('SELECT * FROM af_usuarios WHERE id=?');
    $st->execute([$usuarioId]);
    if ($u = $st->fetch()) verificarConquistas($pdo,$u);
    if ($novoNivel > $nivelAntes) criarNotificacao($pdo,$usuarioId,'level_up','Nível '.$novoNivel.' alcançado!','O resultado aprovado liberou XP e recompensas.',['nivel'=>$novoNivel]);
    criarNotificacao($pdo,$usuarioId,'security_review','Resultado aprovado','A equipe revisou sua sessão e liberou a pontuação para o ranking.',['partida_id'=>$partidaId]);

    return ['score'=>$score,'xp'=>(int)$xp['total'],'posicoes'=>$posicoes];
}

function afRevisarPartida(PDO $pdo, int $partidaId, int $revisorId, string $decisao, string $observacao=''): array
{
    if (!in_array($decisao,['aprovar','rejeitar','monitorar','suspender'],true)) throw new RuntimeException('Decisão inválida.');
    $pdo->beginTransaction();
    try {
        $st=$pdo->prepare('SELECT * FROM af_partidas WHERE id=? FOR UPDATE');$st->execute([$partidaId]);$p=$st->fetch();
        if(!$p)throw new RuntimeException('Partida não encontrada.');
        $statusAnterior=(string)($p['antifraude_status']??'pendente');
        if($statusAnterior==='aprovada' && $decisao==='rejeitar')throw new RuntimeException('Uma partida já creditada não pode ser rejeitada automaticamente. Faça uma auditoria financeira antes de qualquer reversão.');

        $status=$decisao==='aprovar'?'aprovada':($decisao==='rejeitar'?'rejeitada':'revisao');
        $validada=$status==='aprovada'?(float)$p['pontuacao_total']:0.0;
        $creditos=['score'=>$validada,'xp'=>(int)($p['xp_ganho']??0),'posicoes'=>[]];
        if($decisao==='aprovar' && $statusAnterior!=='aprovada') {
            $creditos=afCreditarPartidaAprovadaManualmente($pdo,$p);
            $validada=(float)$creditos['score'];
        } else {
            $pdo->prepare('UPDATE af_partidas SET antifraude_status=?,pontuacao_validada=?,validada_em=IF(?=\'aprovada\',NOW(),NULL) WHERE id=?')
                ->execute([$status,$validada,$status,$partidaId]);
        }

        $pdo->prepare('INSERT INTO af_revisoes_seguranca(partida_id,revisor_id,decisao,observacao,score_antes,criado_em) VALUES (?,?,?,?,?,NOW())')
            ->execute([$partidaId,$revisorId,$decisao,mb_substr($observacao,0,2000),(int)$p['antifraude_score']]);
        if($decisao==='monitorar')$pdo->prepare("UPDATE af_usuarios SET seguranca_status='monitorado',ultima_revisao_seguranca=NOW() WHERE id=?")->execute([(int)$p['usuario_id']]);
        if($decisao==='suspender')$pdo->prepare("UPDATE af_usuarios SET seguranca_status='restrito',suspenso_ate=DATE_ADD(NOW(),INTERVAL 24 HOUR),ultima_revisao_seguranca=NOW() WHERE id=?")->execute([(int)$p['usuario_id']]);
        $pdo->prepare("INSERT INTO af_partida_eventos(partida_id,usuario_id,tipo,valor,dados,criado_em) VALUES (?,?,'antifraude_revisao',?, ?,NOW())")
            ->execute([$partidaId,(int)$p['usuario_id'],$validada,json_encode(['decisao'=>$decisao,'revisor_id'=>$revisorId,'status_anterior'=>$statusAnterior],JSON_UNESCAPED_UNICODE)]);
        if($decisao==='rejeitar')criarNotificacao($pdo,(int)$p['usuario_id'],'security_review','Resultado rejeitado','A revisão confirmou que esta sessão não pode entrar no ranking.',['partida_id'=>$partidaId]);
        $pdo->commit();

        if($decisao==='aprovar' && $statusAnterior!=='aprovada') {
            $st=$pdo->prepare('SELECT * FROM af_partidas WHERE id=?');$st->execute([$partidaId]);$aprovada=$st->fetch();
            if($aprovada){
                try{afRegistrarResultadoBatalha($pdo,$aprovada);afRegistrarResultadoEvento($pdo,$aprovada);afConcluirDesafioViralPorPartida($pdo,$aprovada);}catch(Throwable $e){if(defined('APP_DEBUG')&&APP_DEBUG)error_log('Integração pós-revisão: '.$e->getMessage());}
            }
        }
        return ['status'=>$status,'pontuacao_validada'=>$validada,'creditos'=>$creditos];
    }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
}

