<?php
/**
 * AURA FARMING — Etapa 22
 * Motor central de moderação, denúncias, bloqueios e proteção de menores.
 */

function afModerationConfig(PDO $pdo, string $key, mixed $default = null): mixed
{
    static $cache = [];
    if (array_key_exists($key, $cache)) return $cache[$key];
    try {
        $st = $pdo->prepare('SELECT valor,tipo FROM af_configuracoes WHERE chave=? LIMIT 1');
        $st->execute([$key]);
        $row = $st->fetch();
        if (!$row) return $cache[$key] = $default;
        return $cache[$key] = match ((string)$row['tipo']) {
            'bool' => filter_var($row['valor'], FILTER_VALIDATE_BOOLEAN),
            'int' => (int)$row['valor'],
            'float' => (float)$row['valor'],
            'json' => json_decode((string)$row['valor'], true),
            default => $row['valor'],
        };
    } catch (Throwable $e) {
        return $cache[$key] = $default;
    }
}

function afModerationNormalize(string $text): string
{
    $text = mb_strtolower(trim($text), 'UTF-8');
    $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);
    if (is_string($ascii) && $ascii !== '') $text = $ascii;
    $text = preg_replace('/[^a-z0-9@._:\/\-\s]+/i', ' ', $text) ?? $text;
    return trim(preg_replace('/\s+/', ' ', $text) ?? $text);
}

function afModerationUserIsMinor(PDO $pdo, int $userId): bool
{
    if ($userId <= 0) return false;
    try {
        if (function_exists('afResumoIdade')) {
            $age = afResumoIdade($pdo, $userId);
            $type=(string)($age['tipo_conta']??'unknown');
            if($type==='unknown')return true;
            return $type==='minor' || (isset($age['idade']) && $age['idade'] !== null && (int)$age['idade'] < 18);
        }
        $st = $pdo->prepare("SELECT tipo_conta,data_nascimento FROM af_user_age_profiles WHERE usuario_id=? LIMIT 1");
        $st->execute([$userId]);
        $row = $st->fetch();
        if (!$row) return true;
        if ((string)$row['tipo_conta'] === 'minor') return true;
        return !empty($row['data_nascimento']) && (int)(new DateTimeImmutable($row['data_nascimento']))->diff(new DateTimeImmutable('today'))->y < 18;
    } catch (Throwable $e) {
        return true;
    }
}

function afModerationApprovedContact(PDO $pdo, int $userA, int $userB): bool
{
    if ($userA <= 0 || $userB <= 0) return false;
    $aMinor = afModerationUserIsMinor($pdo, $userA);
    $bMinor = afModerationUserIsMinor($pdo, $userB);
    if (!$aMinor && !$bMinor) return true;
    if ($aMinor && (!function_exists('afApprovedContactCheck') || !afApprovedContactCheck($pdo, $userA, $userB))) return false;
    if ($bMinor && (!function_exists('afApprovedContactCheck') || !afApprovedContactCheck($pdo, $userB, $userA))) return false;
    return true;
}

function afModerationMinorCommentsMode(PDO $pdo, int $minorId): string
{
    if($minorId<=0 || !afModerationUserIsMinor($pdo,$minorId))return 'adult';
    try{
        $st=$pdo->prepare('SELECT comments_mode FROM af_minor_safety_settings WHERE minor_usuario_id=? LIMIT 1');
        $st->execute([$minorId]);
        $mode=(string)($st->fetchColumn()?:afModerationConfig($pdo,'moderation_minor_comments_default','disabled'));
        return in_array($mode,['disabled','moderated'],true)?$mode:'disabled';
    }catch(Throwable $e){return 'disabled';}
}

function afModerationCommentsEnabledForMatch(PDO $pdo, array $partida, int $viewerId): bool
{
    if(!afModerationConfig($pdo,'moderation_live_comments_enabled',true))return false;
    $ownerId=(int)($partida['usuario_id']??0);
    if($ownerId<=0 || $viewerId<=0)return false;
    try{afModerationAssertCanViewLive($pdo,$partida,$viewerId);}catch(Throwable $e){return false;}
    if(afModerationUserIsMinor($pdo,$ownerId) && afModerationMinorCommentsMode($pdo,$ownerId)!=='moderated')return false;
    if(afModerationUserIsMinor($pdo,$viewerId) && afModerationMinorCommentsMode($pdo,$viewerId)!=='moderated')return false;
    if(($ownerId!==$viewerId) && (afModerationUserIsMinor($pdo,$ownerId)||afModerationUserIsMinor($pdo,$viewerId)) && !afModerationApprovedContact($pdo,$ownerId,$viewerId))return false;
    return true;
}

function afModerationProtocol(): string
{
    return 'SAFE-' . date('ymd') . '-' . strtoupper(bin2hex(random_bytes(4)));
}

function afModerationSeverityScore(string $severity): int
{
    return match ($severity) {
        'critical' => 90,
        'high' => 70,
        'medium' => 40,
        default => 15,
    };
}

function afModerationScanContent(PDO $pdo, string $text, string $scope = 'comments', int $authorId = 0, int $targetId = 0): array
{
    $original = trim(strip_tags($text));
    $original = mb_substr($original, 0, 500);
    $normalized = afModerationNormalize($original);
    $minorContext = afModerationUserIsMinor($pdo, $authorId) || afModerationUserIsMinor($pdo, $targetId);
    $visible = $original;
    $hits = [];
    $risk = 0;
    $strongest = 'allow';
    $actionRank = ['allow'=>0,'mask'=>1,'review'=>2,'block'=>3,'suspend'=>4];

    $addHit = static function (string $type, string $value, string $severity, string $action) use (&$hits, &$risk, &$strongest, $actionRank): void {
        $hits[] = ['type'=>$type,'value'=>$value,'severity'=>$severity,'action'=>$action];
        $risk = max($risk, afModerationSeverityScore($severity));
        if (($actionRank[$action] ?? 0) > ($actionRank[$strongest] ?? 0)) $strongest = $action;
    };

    if (preg_match('~(?:https?://|www\.|[a-z0-9-]+\.(?:com|net|org|br|io|app)(?:/|\b))~iu', $original)) {
        $action = $minorContext && afModerationConfig($pdo,'moderation_links_blocked_for_minors',true) ? 'block' : 'review';
        $addHit('link','link','high',$action);
    }
    if (preg_match('/(?:\+?55\s*)?(?:\(?\d{2}\)?\s*)?9?\d{4}[\s.-]?\d{4}/u', $original)) {
        $addHit('phone','telefone','critical',$minorContext ? 'block' : 'review');
    }
    if (preg_match('/\b(?:whatsapp|zap|instagram|insta|telegram|discord|tiktok|facebook)\b/iu', $normalized)) {
        $addHit('social_request','rede social','high',$minorContext ? 'block' : 'review');
    }
    if (preg_match('/\b(?:onde voce mora|qual sua cidade|manda localizacao|manda endereco|seu endereco|sua escola)\b/iu', $normalized)) {
        $addHit('location_request','localização','critical',$minorContext ? 'block' : 'review');
    }

    try {
        $allowedScopes = ['global', in_array($scope,['comments','messages','live','minor'],true)?$scope:'global'];
        if ($minorContext) $allowedScopes[] = 'minor';
        $placeholders = implode(',', array_fill(0,count(array_unique($allowedScopes)),'?'));
        $st = $pdo->prepare("SELECT * FROM af_content_filters WHERE active=1 AND scope IN ({$placeholders}) AND (minor_only=0 OR ?=1) ORDER BY FIELD(severity,'critical','high','medium','low'),id");
        $params = array_values(array_unique($allowedScopes));
        $params[] = $minorContext ? 1 : 0;
        $st->execute($params);
        foreach ($st->fetchAll() ?: [] as $filter) {
            $pattern = (string)$filter['pattern_value'];
            $normalizedPattern = (string)($filter['normalized_value'] ?: afModerationNormalize($pattern));
            $matched = false;
            if ((string)$filter['pattern_type'] === 'regex') {
                $matched = @preg_match('~'.$pattern.'~iu', $original) === 1;
            } elseif ((string)$filter['pattern_type'] === 'phrase') {
                $matched = $normalizedPattern !== '' && str_contains($normalized, $normalizedPattern);
            } else {
                $matched = $normalizedPattern !== '' && preg_match('/(?:^|\s)'.preg_quote($normalizedPattern,'/').'(?:$|\s)/iu', $normalized) === 1;
            }
            if (!$matched) continue;
            $action = (string)$filter['action'];
            $severity = (string)$filter['severity'];
            $addHit('filter', $pattern, $severity, $action);
            if (in_array($action,['mask','review'],true)) {
                if ((string)$filter['pattern_type'] === 'regex') {
                    $visible = @preg_replace('~'.$pattern.'~iu', (string)$filter['replacement'], $visible) ?? $visible;
                } else {
                    $visible = str_ireplace($pattern, (string)$filter['replacement'], $visible);
                }
            }
        }
    } catch (Throwable $e) {
        if (defined('APP_DEBUG') && APP_DEBUG) error_log('[AURA-MODERATION-FILTER] '.$e->getMessage());
    }

    if ($strongest === 'review' && $risk < 50) $risk = 50;
    if ($strongest === 'block' && $risk < 70) $risk = 70;
    if ($strongest === 'suspend' && $risk < 90) $risk = 90;
    return [
        'original'=>$original,
        'visible'=>$visible,
        'normalized'=>$normalized,
        'minor_context'=>$minorContext,
        'hits'=>$hits,
        'risk_score'=>min(100,$risk),
        'action'=>$strongest,
        'allowed'=>!in_array($strongest,['block','suspend'],true),
        'status'=>match($strongest){'mask'=>'masked','review'=>'review','block','suspend'=>'blocked',default=>'visible'},
    ];
}

function afModerationBlockUser(PDO $pdo, int $blockerId, int $blockedId, string $source = 'user', ?int $reportId = null, string $reason = ''): void
{
    if ($blockerId <= 0 || $blockedId <= 0 || $blockerId === $blockedId) throw new RuntimeException('Bloqueio inválido.');
    $allowedSources = ['user','guardian','moderation','automatic'];
    if (!in_array($source,$allowedSources,true)) $source = 'user';
    $pdo->prepare("INSERT INTO af_user_blocks(blocker_user_id,blocked_user_id,source,report_id,reason,active,created_at,updated_at)
        VALUES (?,?,?,?,?,1,NOW(),NOW()) ON DUPLICATE KEY UPDATE source=VALUES(source),report_id=VALUES(report_id),reason=VALUES(reason),active=1,revoked_at=NULL,updated_at=NOW()")
        ->execute([$blockerId,$blockedId,$source,$reportId,$reason !== '' ? mb_substr($reason,0,500) : null]);
    $pdo->prepare('DELETE FROM af_seguidores WHERE (seguidor_id=? AND seguido_id=?) OR (seguidor_id=? AND seguido_id=?)')
        ->execute([$blockerId,$blockedId,$blockedId,$blockerId]);
}

function afModerationUnblockUser(PDO $pdo, int $blockerId, int $blockedId): void
{
    $pdo->prepare('UPDATE af_user_blocks SET active=0,revoked_at=NOW(),updated_at=NOW() WHERE blocker_user_id=? AND blocked_user_id=?')
        ->execute([$blockerId,$blockedId]);
}

function afModerationHasBlocked(PDO $pdo, int $blockerId, int $blockedId): bool
{
    if($blockerId<=0||$blockedId<=0)return false;
    try{$st=$pdo->prepare('SELECT 1 FROM af_user_blocks WHERE blocker_user_id=? AND blocked_user_id=? AND active=1 LIMIT 1');$st->execute([$blockerId,$blockedId]);return (bool)$st->fetchColumn();}
    catch(Throwable $e){return false;}
}

function afModerationIsBlocked(PDO $pdo, int $userA, int $userB): bool
{
    return afModerationHasBlocked($pdo,$userA,$userB)||afModerationHasBlocked($pdo,$userB,$userA);
}

function afModerationCanUsersInteract(PDO $pdo, int $userA, int $userB, bool $approvedContactRequired = false): bool
{
    if ($userA <= 0 || $userB <= 0 || $userA === $userB) return true;
    if (afModerationIsBlocked($pdo,$userA,$userB)) return false;
    if ($approvedContactRequired && !afModerationApprovedContact($pdo,$userA,$userB)) return false;
    return true;
}

function afModerationIsSpectatorRestricted(PDO $pdo, int $partidaId, int $spectatorId): bool
{
    try {
        $st=$pdo->prepare("SELECT 1 FROM af_live_spectator_restrictions WHERE partida_id=? AND spectator_user_id=? AND active=1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>NOW()) LIMIT 1");
        $st->execute([$partidaId,$spectatorId]);
        return (bool)$st->fetchColumn();
    } catch(Throwable $e){ return false; }
}

function afModerationAssertCanViewLive(PDO $pdo, array $partida, int $viewerId): void
{
    $ownerId=(int)($partida['usuario_id']??0);
    if($viewerId<=0 || $ownerId<=0 || $viewerId===$ownerId)return;
    if(afModerationIsBlocked($pdo,$ownerId,$viewerId))throw new RuntimeException('Esta transmissão não está disponível para esta conta.');
    if(afModerationIsSpectatorRestricted($pdo,(int)$partida['id'],$viewerId))throw new RuntimeException('Você foi removido desta transmissão.');
    if((afModerationUserIsMinor($pdo,$ownerId)||afModerationUserIsMinor($pdo,$viewerId)) && !afModerationApprovedContact($pdo,$ownerId,$viewerId)){
        throw new RuntimeException('Transmissões envolvendo menores são limitadas a contatos aprovados.');
    }
}

function afModerationCreateMinorIncident(PDO $pdo, int $minorId, ?int $relatedId, ?int $reportId, ?int $partidaId, string $type, string $severity, string $summary, array $details=[]): int
{
    if($minorId<=0)return 0;
    $types=['unknown_private_contact','link_attempt','phone_request','social_request','location_request','inappropriate_comment','harassment','sexual_content','blocked_access','other'];
    $severities=['info','warning','high','critical'];
    if(!in_array($type,$types,true))$type='other';
    if(!in_array($severity,$severities,true))$severity='warning';
    $st=$pdo->prepare("INSERT INTO af_minor_safety_incidents(minor_user_id,related_user_id,report_id,partida_id,incident_type,severity,summary,details_json,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?, 'open',NOW(),NOW())");
    $st->execute([$minorId,$relatedId?:null,$reportId?:null,$partidaId?:null,$type,$severity,mb_substr($summary,0,500),$details?json_encode($details,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES):null]);
    $id=(int)$pdo->lastInsertId();
    if(function_exists('afGuardianDashboardCreateAlert') && in_array($severity,['high','critical'],true)){
        afGuardianDashboardCreateAlert($pdo,$minorId,'serious_incident',$severity==='critical'?'critical':'warning','Incidente de segurança','Uma interação envolvendo o dependente foi bloqueada e enviada para análise.',['incident_id'=>$id,'type'=>$type],'minor_safety_incident',$id);
        $pdo->prepare('UPDATE af_minor_safety_incidents SET guardian_notified_at=NOW() WHERE id=?')->execute([$id]);
    }
    return $id;
}


function afModerationAssertPrivateMessageAllowed(PDO $pdo, int $senderId, int $recipientId, string $body): array
{
    if($senderId<=0||$recipientId<=0||$senderId===$recipientId)throw new RuntimeException('Destinatário inválido.');
    if(afModerationIsBlocked($pdo,$senderId,$recipientId))throw new RuntimeException('Mensagens não estão disponíveis entre estas contas.');
    $minorContext=afModerationUserIsMinor($pdo,$senderId)||afModerationUserIsMinor($pdo,$recipientId);
    if($minorContext && !afModerationApprovedContact($pdo,$senderId,$recipientId)){
        $minorId=afModerationUserIsMinor($pdo,$senderId)?$senderId:$recipientId;
        afModerationCreateMinorIncident($pdo,$minorId,$minorId===$senderId?$recipientId:$senderId,null,null,'unknown_private_contact','high','Tentativa de mensagem privada com contato não aprovado.');
        throw new RuntimeException('Menores só podem trocar mensagens com contatos aprovados.');
    }
    $scan=afModerationScanContent($pdo,$body,'messages',$senderId,$recipientId);
    if(!$scan['allowed']){
        if($minorContext){$minorId=afModerationUserIsMinor($pdo,$senderId)?$senderId:$recipientId;$types=array_column($scan['hits'],'type');$type=in_array('location_request',$types,true)?'location_request':(in_array('phone',$types,true)?'phone_request':(in_array('social_request',$types,true)?'social_request':(in_array('link',$types,true)?'link_attempt':'inappropriate_comment')));afModerationCreateMinorIncident($pdo,$minorId,$minorId===$senderId?$recipientId:$senderId,null,null,$type,(int)$scan['risk_score']>=90?'critical':'high','Mensagem privada bloqueada pelo filtro de segurança.',['hits'=>$scan['hits']]);}
        throw new RuntimeException('A mensagem foi bloqueada pelas regras de segurança.');
    }
    return $scan;
}

function afModerationCreateReport(PDO $pdo, int $reporterId, array $data): array
{
    afRateLimitOrFail($pdo,'safety_report',$reporterId.'|'.clientIp(),8,3600,3600);
    $targetId=(int)($data['reported_user_id']??0);
    $partidaId=(int)($data['partida_id']??0);
    $competitionId=(int)($data['competition_id']??0);
    $commentId=(int)($data['comment_id']??0);
    if($targetId===$reporterId && $targetId>0)throw new RuntimeException('Não é possível denunciar a própria conta.');
    $types=['player','transmission','comment','message','spectator','other'];
    $categories=['harassment','sexual_content','violence','hate','spam','fraud','impersonation','contact_request','social_request','location_request','link_sharing','privacy','other'];
    $type=(string)($data['report_type']??'player'); if(!in_array($type,$types,true))$type='other';
    $category=(string)($data['category']??'other'); if(!in_array($category,$categories,true))$category='other';
    $description=trim(strip_tags((string)($data['description']??'')));
    if(mb_strlen($description)<10)throw new RuntimeException('Descreva o ocorrido com pelo menos 10 caracteres.');
    $description=mb_substr($description,0,3000);
    if($commentId>0){$st=$pdo->prepare('SELECT author_user_id,partida_id FROM af_live_comments WHERE id=? LIMIT 1');$st->execute([$commentId]);$comment=$st->fetch();if(!$comment)throw new RuntimeException('Comentário não encontrado.');$targetId=(int)$comment['author_user_id'];$partidaId=(int)$comment['partida_id'];$type='comment';}
    if($partidaId>0){$st=$pdo->prepare('SELECT usuario_id FROM af_partidas WHERE id=? LIMIT 1');$st->execute([$partidaId]);$ownerId=(int)($st->fetchColumn()?:0);if($ownerId<=0)throw new RuntimeException('Partida ou transmissão não encontrada.');if($targetId<=0&&$type!=='spectator')$targetId=$ownerId;}
    if($competitionId>0){$st=$pdo->prepare('SELECT id FROM af_competicoes WHERE id=? LIMIT 1');$st->execute([$competitionId]);if(!$st->fetchColumn())throw new RuntimeException('Competição não encontrada.');}
    if($targetId>0){$st=$pdo->prepare('SELECT id FROM af_usuarios WHERE id=? LIMIT 1');$st->execute([$targetId]);if(!$st->fetchColumn())throw new RuntimeException('Usuário denunciado não encontrado.');}
    $reporterMinor=afModerationUserIsMinor($pdo,$reporterId);$targetMinor=$targetId>0&&afModerationUserIsMinor($pdo,$targetId);$involvesMinor=$reporterMinor||$targetMinor;
    $baseRisk=match($category){'sexual_content'=>95,'location_request'=>90,'contact_request','social_request'=>80,'harassment','violence','hate'=>75,'privacy','link_sharing'=>70,'fraud','impersonation'=>60,'spam'=>30,default=>40};
    $scan=afModerationScanContent($pdo,$description,'messages',$reporterId,$targetId);
    $risk=max($baseRisk,(int)$scan['risk_score']);
    $priority=match(true){$risk>=90=>'critical',$risk>=70=>'high',$risk>=40=>'medium',default=>'low'};
    $protocol=afModerationProtocol();
    $context=['ip_hash'=>hash('sha256',clientIp().'|'.(defined('SECURITY_HASH_SALT')?SECURITY_HASH_SALT:'')),'device_hash'=>hash('sha256',deviceUuid()),'user_agent_hash'=>hash('sha256',(string)($_SERVER['HTTP_USER_AGENT']??'')),'scan'=>$scan];
    $evidence=is_array($data['evidence']??null)?$data['evidence']:[];
    $st=$pdo->prepare("INSERT INTO af_safety_reports(protocol,reporter_user_id,reported_user_id,partida_id,competition_id,comment_id,report_type,category,description,evidence_json,context_json,status,priority,risk_score,involves_minor,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,'open',?,?,?,NOW(),NOW())");
    $st->execute([$protocol,$reporterId?:null,$targetId?:null,$partidaId?:null,$competitionId?:null,$commentId?:null,$type,$category,$description,$evidence?json_encode($evidence,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES):null,json_encode($context,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),$priority,$risk,$involvesMinor?1:0]);
    $id=(int)$pdo->lastInsertId();
    if($targetId>0 && ($data['block_user']??false))afModerationBlockUser($pdo,$reporterId,$targetId,'user',$id,'Bloqueado ao enviar denúncia');
    if($involvesMinor){
        $minorId=$reporterMinor?$reporterId:$targetId;
        $incidentType=match($category){'contact_request'=>'phone_request','social_request'=>'social_request','location_request'=>'location_request','link_sharing'=>'link_attempt','sexual_content'=>'sexual_content','harassment'=>'harassment',default=>'other'};
        afModerationCreateMinorIncident($pdo,$minorId,$reporterMinor?$targetId:$reporterId,$id,$partidaId,$incidentType,$priority==='critical'?'critical':($priority==='high'?'high':'warning'),'Denúncia de segurança envolvendo menor',['protocol'=>$protocol,'category'=>$category]);
    }
    afModerationEvaluateAutomaticAction($pdo,$targetId,$id,$risk,false);
    return ['id'=>$id,'protocol'=>$protocol,'priority'=>$priority,'risk_score'=>$risk];
}

function afModerationEvaluateAutomaticAction(PDO $pdo, int $targetId, int $reportId, int $riskScore, bool $trustedDetection = false): void
{
    if($targetId<=0 || !afModerationConfig($pdo,'moderation_auto_suspend_enabled',true))return;
    $threshold=max(2,(int)afModerationConfig($pdo,'moderation_repeat_reports_threshold',3));
    $st=$pdo->prepare("SELECT COUNT(DISTINCT reporter_user_id) FROM af_safety_reports WHERE reported_user_id=? AND status NOT IN ('archived') AND created_at>=DATE_SUB(NOW(),INTERVAL 30 DAY)");
    $st->execute([$targetId]);$repeat=(int)$st->fetchColumn();
    if($trustedDetection){if($riskScore<90)return;}elseif($repeat<$threshold)return;
    $active=$pdo->prepare("SELECT 1 FROM af_moderation_actions WHERE target_user_id=? AND action_type='preventive_suspend' AND revoked_at IS NULL AND (ends_at IS NULL OR ends_at>NOW()) LIMIT 1");
    $active->execute([$targetId]);if($active->fetchColumn())return;
    $hours=max(1,min(168,(int)afModerationConfig($pdo,'moderation_auto_suspend_hours',24)));
    $pdo->prepare("UPDATE af_usuarios SET seguranca_status='restrito',suspenso_ate=GREATEST(COALESCE(suspenso_ate,NOW()),DATE_ADD(NOW(),INTERVAL {$hours} HOUR)),ultima_revisao_seguranca=NOW(),atualizado_em=NOW() WHERE id=? AND seguranca_status<>'banido'")->execute([$targetId]);
    $pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,action_type,reason,starts_at,ends_at,metadata_json,created_at) VALUES (?,?,'preventive_suspend','Suspensão preventiva automática por risco crítico ou reincidência.',NOW(),DATE_ADD(NOW(),INTERVAL {$hours} HOUR),?,NOW())")
        ->execute([$reportId?:null,$targetId,json_encode(['risk_score'=>$riskScore,'distinct_reporters'=>$repeat],JSON_UNESCAPED_UNICODE)]);
    criarNotificacao($pdo,$targetId,'moderation_suspend','Conta em análise','O acesso foi suspenso preventivamente enquanto a equipe analisa uma denúncia de segurança.',['report_id'=>$reportId]);
}

function afModerationCreateComment(PDO $pdo, array $partida, int $authorId, string $body): array
{
    afModerationAssertCanViewLive($pdo,$partida,$authorId);
    $ownerId=(int)$partida['usuario_id'];
    $ownerMinor=afModerationUserIsMinor($pdo,$ownerId);$authorMinor=afModerationUserIsMinor($pdo,$authorId);
    if(!afModerationCommentsEnabledForMatch($pdo,$partida,$authorId))throw new RuntimeException('Comentários estão desativados ou não autorizados para esta conta.');
    afRateLimitOrFail($pdo,'live_comment',$authorId.'|'.(int)$partida['id'],6,60,180);
    $scan=afModerationScanContent($pdo,$body,'comments',$authorId,$ownerId);
    if(trim($scan['original'])==='')throw new RuntimeException('Digite um comentário.');
    $status=(string)$scan['status'];
    $visible=(string)$scan['visible'];
    if(!$scan['allowed']){
        $minorId=$authorMinor?$authorId:($ownerMinor?$ownerId:0);
        if($minorId>0){$hitTypes=array_column($scan['hits'],'type');$type=in_array('location_request',$hitTypes,true)?'location_request':(in_array('phone',$hitTypes,true)?'phone_request':(in_array('social_request',$hitTypes,true)?'social_request':(in_array('link',$hitTypes,true)?'link_attempt':'inappropriate_comment')));afModerationCreateMinorIncident($pdo,$minorId,$authorId=== $minorId?$ownerId:$authorId,null,(int)$partida['id'],$type,(int)$scan['risk_score']>=90?'critical':'high','Comentário bloqueado pelo filtro de segurança',['hits'=>$scan['hits']]);}
        if($scan['action']==='suspend')afModerationEvaluateAutomaticAction($pdo,$authorId,0,(int)$scan['risk_score'],true);
    }
    $st=$pdo->prepare("INSERT INTO af_live_comments(partida_id,author_user_id,body_original,body_visible,status,risk_score,filter_hits_json,created_at,updated_at) VALUES (?,?,?,?,?,?,?,NOW(),NOW())");
    $st->execute([(int)$partida['id'],$authorId,$scan['original'],$visible,$status,(int)$scan['risk_score'],$scan['hits']?json_encode($scan['hits'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES):null]);
    $id=(int)$pdo->lastInsertId();
    if(!$scan['allowed'])throw new RuntimeException('Comentário bloqueado pelas regras de segurança.');
    return ['id'=>$id,'body'=>$visible,'status'=>$status,'risk_score'=>(int)$scan['risk_score']];
}

function afModerationComments(PDO $pdo, array $partida, int $viewerId, int $afterId = 0, int $limit = 40): array
{
    afModerationAssertCanViewLive($pdo,$partida,$viewerId);
    if(!afModerationCommentsEnabledForMatch($pdo,$partida,$viewerId))return [];
    $limit=max(1,min(80,$limit));
    $st=$pdo->prepare("SELECT c.id,c.body_visible body,c.status,c.created_at,u.id author_id,u.nome,u.username,u.aura_id,u.foto_perfil,u.foto_externa
        FROM af_live_comments c JOIN af_usuarios u ON u.id=c.author_user_id
        WHERE c.partida_id=? AND c.id>? AND c.status IN ('visible','masked')
          AND NOT EXISTS(SELECT 1 FROM af_user_blocks b WHERE b.active=1 AND ((b.blocker_user_id=? AND b.blocked_user_id=c.author_user_id) OR (b.blocker_user_id=c.author_user_id AND b.blocked_user_id=?)))
        ORDER BY c.id ASC LIMIT {$limit}");
    $st->execute([(int)$partida['id'],$afterId,$viewerId,$viewerId]);
    $rows=$st->fetchAll()?:[];
    foreach($rows as &$row){$row['avatar']=urlAvatar($row['foto_perfil']??null,$row['foto_externa']??null);unset($row['foto_perfil'],$row['foto_externa']);}
    return $rows;
}

function afModerationLiveViewers(PDO $pdo, int $ownerId, int $partidaId): array
{
    $st=$pdo->prepare('SELECT id FROM af_partidas WHERE id=? AND usuario_id=? LIMIT 1');$st->execute([$partidaId,$ownerId]);if(!$st->fetchColumn())throw new RuntimeException('Transmissão não encontrada.');
    $timeout=(int)(defined('LIVE_PRESENCE_TIMEOUT')?LIVE_PRESENCE_TIMEOUT:12);
    $st=$pdo->prepare("SELECT e.usuario_id,u.nome,u.username,u.aura_id,u.foto_perfil,u.foto_externa,e.entrou_em,e.ultimo_ping,
        EXISTS(SELECT 1 FROM af_live_spectator_restrictions r WHERE r.partida_id=e.partida_id AND r.spectator_user_id=e.usuario_id AND r.active=1 AND r.revoked_at IS NULL) restricted
        FROM af_partida_espectadores e JOIN af_usuarios u ON u.id=e.usuario_id
        WHERE e.partida_id=? AND e.saiu_em IS NULL AND e.ultimo_ping>=DATE_SUB(NOW(),INTERVAL {$timeout} SECOND)
        ORDER BY e.entrou_em DESC");$st->execute([$partidaId]);$rows=$st->fetchAll()?:[];
    foreach($rows as &$row){$row['avatar']=urlAvatar($row['foto_perfil']??null,$row['foto_externa']??null);unset($row['foto_perfil'],$row['foto_externa']);}
    return $rows;
}

function afModerationKickSpectator(PDO $pdo, int $ownerId, int $partidaId, int $spectatorId, bool $block = false, string $reason = ''): void
{
    if($spectatorId<=0||$spectatorId===$ownerId)throw new RuntimeException('Espectador inválido.');
    $st=$pdo->prepare('SELECT id FROM af_partidas WHERE id=? AND usuario_id=? LIMIT 1');$st->execute([$partidaId,$ownerId]);if(!$st->fetchColumn())throw new RuntimeException('Transmissão não encontrada.');
    $type=$block?'blocked':'kicked';
    $pdo->prepare("INSERT INTO af_live_spectator_restrictions(partida_id,owner_user_id,spectator_user_id,restriction_type,reason,active,created_at) VALUES (?,?,?,?,?,1,NOW()) ON DUPLICATE KEY UPDATE restriction_type=VALUES(restriction_type),reason=VALUES(reason),active=1,expires_at=NULL,revoked_at=NULL,created_at=NOW()")
        ->execute([$partidaId,$ownerId,$spectatorId,$type,$reason!==''?mb_substr($reason,0,500):null]);
    $pdo->prepare('UPDATE af_partida_espectadores SET saiu_em=NOW(),ultimo_ping=NOW() WHERE partida_id=? AND usuario_id=?')->execute([$partidaId,$spectatorId]);
    $pdo->prepare('DELETE FROM af_live_sinais WHERE partida_id=? AND (remetente_id=? OR destinatario_id=?)')->execute([$partidaId,$spectatorId,$spectatorId]);
    $pdo->prepare("INSERT INTO af_moderation_actions(target_user_id,target_partida_id,action_type,reason,metadata_json,created_at) VALUES (?,?, 'spectator_kick',?,?,NOW())")
        ->execute([$spectatorId,$partidaId,$reason!==''?$reason:'Removido pelo responsável pela transmissão',json_encode(['block'=>$block,'owner_id'=>$ownerId],JSON_UNESCAPED_UNICODE)]);
    if($block)afModerationBlockUser($pdo,$ownerId,$spectatorId,'user',null,'Bloqueado durante transmissão');
}

function afModerationNotifyGuardianAction(PDO $pdo, int $targetId, string $action, string $reason, int $reportId): void
{
    if($targetId<=0 || !afModerationUserIsMinor($pdo,$targetId) || !function_exists('afGuardianDashboardCreateAlert'))return;
    $severity=in_array($action,['preventive_suspend','temporary_suspend','permanent_ban'],true)?'critical':'warning';
    $title=$action==='warning'?'Aviso da moderação':'Ação de segurança na conta';
    $message=$action==='warning'?'O dependente recebeu um aviso da equipe de moderação.':'A conta do dependente recebeu uma restrição de segurança e está em análise.';
    afGuardianDashboardCreateAlert($pdo,$targetId,'serious_incident',$severity,$title,$message,['action'=>$action,'reason'=>$reason,'report_id'=>$reportId],'safety_report',$reportId);
}

function afModerationAdminApplyAction(PDO $pdo, int $adminId, int $reportId, string $action, string $reason, int $durationHours = 24): void
{
    $st=$pdo->prepare('SELECT * FROM af_safety_reports WHERE id=? LIMIT 1');$st->execute([$reportId]);$report=$st->fetch();if(!$report)throw new RuntimeException('Denúncia não encontrada.');
    $targetId=(int)($report['reported_user_id']??0);$reason=trim(strip_tags($reason));if(mb_strlen($reason)<3)$reason='Ação aplicada pela equipe de moderação.';
    $valid=['take_review','warning','hide_content','preventive_suspend','temporary_suspend','permanent_ban','resolve','archive','restore'];if(!in_array($action,$valid,true))throw new RuntimeException('Ação inválida.');
    $pdo->beginTransaction();
    try{
        if($action==='take_review'){$pdo->prepare("UPDATE af_safety_reports SET status='in_review',assigned_admin_id=?,updated_at=NOW() WHERE id=?")->execute([$adminId,$reportId]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,admin_user_id,action_type,reason,metadata_json,created_at) VALUES (?,?,?,'other',?,? ,NOW())")->execute([$reportId,$targetId?:null,$adminId,$reason,json_encode(['status'=>'in_review'],JSON_UNESCAPED_UNICODE)]);}
        elseif($action==='warning'){if($targetId)criarNotificacao($pdo,$targetId,'moderation_warning','Aviso da moderação',$reason,['report_protocol'=>$report['protocol']]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,admin_user_id,action_type,reason,created_at) VALUES (?,?,?,'warning',?,NOW())")->execute([$reportId,$targetId?:null,$adminId,$reason]);$pdo->prepare("UPDATE af_safety_reports SET status='acted',assigned_admin_id=?,resolution=?,updated_at=NOW() WHERE id=?")->execute([$adminId,$reason,$reportId]);afModerationNotifyGuardianAction($pdo,$targetId,'warning',$reason,$reportId);}
        elseif($action==='hide_content'){if(!empty($report['comment_id']))$pdo->prepare("UPDATE af_live_comments SET status='hidden',hidden_by_admin_id=?,updated_at=NOW() WHERE id=?")->execute([$adminId,(int)$report['comment_id']]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,target_partida_id,target_comment_id,admin_user_id,action_type,reason,created_at) VALUES (?,?,?,?,?,'content_hide',?,NOW())")->execute([$reportId,$targetId?:null,$report['partida_id']?:null,$report['comment_id']?:null,$adminId,$reason]);$pdo->prepare("UPDATE af_safety_reports SET status='acted',assigned_admin_id=?,resolution=?,updated_at=NOW() WHERE id=?")->execute([$adminId,$reason,$reportId]);}
        elseif(in_array($action,['preventive_suspend','temporary_suspend'],true)){if(!$targetId)throw new RuntimeException('A denúncia não possui usuário alvo.');$hours=max(1,min(720,$durationHours));$pdo->prepare("UPDATE af_usuarios SET seguranca_status='restrito',suspenso_ate=DATE_ADD(NOW(),INTERVAL {$hours} HOUR),ultima_revisao_seguranca=NOW(),atualizado_em=NOW() WHERE id=?")->execute([$targetId]);$pdo->prepare("UPDATE af_sessoes SET revogada_em=NOW() WHERE usuario_id=? AND revogada_em IS NULL")->execute([$targetId]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,admin_user_id,action_type,reason,starts_at,ends_at,created_at) VALUES (?,?,?,?,?,NOW(),DATE_ADD(NOW(),INTERVAL {$hours} HOUR),NOW())")->execute([$reportId,$targetId,$adminId,$action,$reason]);$pdo->prepare("UPDATE af_safety_reports SET status='acted',assigned_admin_id=?,resolution=?,updated_at=NOW() WHERE id=?")->execute([$adminId,$reason,$reportId]);afModerationNotifyGuardianAction($pdo,$targetId,$action,$reason,$reportId);}
        elseif($action==='permanent_ban'){if(!$targetId)throw new RuntimeException('A denúncia não possui usuário alvo.');$pdo->prepare("UPDATE af_usuarios SET status=2,seguranca_status='banido',suspenso_ate=NULL,ultima_revisao_seguranca=NOW(),atualizado_em=NOW() WHERE id=?")->execute([$targetId]);$pdo->prepare("UPDATE af_sessoes SET revogada_em=NOW() WHERE usuario_id=? AND revogada_em IS NULL")->execute([$targetId]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,admin_user_id,action_type,reason,created_at) VALUES (?,?,?,'permanent_ban',?,NOW())")->execute([$reportId,$targetId,$adminId,$reason]);$pdo->prepare("UPDATE af_safety_reports SET status='acted',assigned_admin_id=?,resolution=?,updated_at=NOW() WHERE id=?")->execute([$adminId,$reason,$reportId]);afModerationNotifyGuardianAction($pdo,$targetId,'permanent_ban',$reason,$reportId);}
        elseif($action==='restore'){if($targetId)$pdo->prepare("UPDATE af_usuarios SET status=1,seguranca_status='normal',suspenso_ate=NULL,ultima_revisao_seguranca=NOW(),atualizado_em=NOW() WHERE id=?")->execute([$targetId]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,admin_user_id,action_type,reason,created_at) VALUES (?,?,?,'restore',?,NOW())")->execute([$reportId,$targetId?:null,$adminId,$reason]);}
        elseif($action==='resolve'){$pdo->prepare("UPDATE af_safety_reports SET status='resolved',assigned_admin_id=?,resolution=?,resolved_at=NOW(),updated_at=NOW() WHERE id=?")->execute([$adminId,$reason,$reportId]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,admin_user_id,action_type,reason,created_at) VALUES (?,?,?,'close_report',?,NOW())")->execute([$reportId,$targetId?:null,$adminId,$reason]);}
        elseif($action==='archive'){$pdo->prepare("UPDATE af_safety_reports SET status='archived',assigned_admin_id=?,resolution=?,resolved_at=NOW(),updated_at=NOW() WHERE id=?")->execute([$adminId,$reason,$reportId]);$pdo->prepare("INSERT INTO af_moderation_actions(report_id,target_user_id,admin_user_id,action_type,reason,metadata_json,created_at) VALUES (?,?,?,'close_report',?,?,NOW())")->execute([$reportId,$targetId?:null,$adminId,$reason,json_encode(['status'=>'archived'],JSON_UNESCAPED_UNICODE)]);}
        $pdo->commit();
    }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
}

function afModerationReportLabels(): array
{
    return [
        'harassment'=>'Assédio ou intimidação','sexual_content'=>'Conteúdo sexual ou aliciamento','violence'=>'Violência ou ameaça','hate'=>'Discurso de ódio','spam'=>'Spam','fraud'=>'Fraude','impersonation'=>'Identidade falsa','contact_request'=>'Pedido de telefone','social_request'=>'Pedido de rede social','location_request'=>'Pedido de localização','link_sharing'=>'Envio de link','privacy'=>'Violação de privacidade','other'=>'Outro motivo',
    ];
}
