<?php
/**
 * AURA FARMING — Etapa 23
 * Eventos infantis controlados, supervisionados, sem transmissão pública e com retenção limitada.
 */

function afControlledEventConfig(PDO $pdo,string $key,mixed $default=null): mixed
{
    if(function_exists('afMinorPolicyConfig')) return afMinorPolicyConfig($pdo,$key,$default);
    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]);$r=$st->fetch();if(!$r)return $cache[$key]=$default;
        return $cache[$key]=match((string)$r['tipo']){'bool'=>filter_var($r['valor'],FILTER_VALIDATE_BOOLEAN),'int'=>(int)$r['valor'],'float'=>(float)$r['valor'],'json'=>json_decode((string)$r['valor'],true),default=>$r['valor']};
    }catch(Throwable){return $cache[$key]=$default;}
}

function afControlledEventEnabled(PDO $pdo): bool
{
    return filter_var(afControlledEventConfig($pdo,'controlled_event_enabled',false),FILTER_VALIDATE_BOOLEAN);
}

function afControlledEventRandomCode(PDO $pdo,string $table,string $column,int $bytes=8): string
{
    do{$value=strtoupper(substr(bin2hex(random_bytes($bytes)),0,$bytes*2));$st=$pdo->prepare("SELECT id FROM `{$table}` WHERE `{$column}`=? LIMIT 1");$st->execute([$value]);}while($st->fetchColumn());
    return $value;
}

function afControlledEventOrganizer(PDO $pdo,int $userId): ?array
{
    $st=$pdo->prepare('SELECT o.*,u.nome,u.email,u.aura_id,a.idade_declarada,a.status_idade FROM af_controlled_event_organizers o JOIN af_usuarios u ON u.id=o.usuario_id LEFT JOIN af_user_age_profiles a ON a.usuario_id=u.id WHERE o.usuario_id=? LIMIT 1');
    $st->execute([$userId]);return $st->fetch()?:null;
}

function afControlledEventOrganizerApproved(PDO $pdo,int $userId): bool
{
    $o=afControlledEventOrganizer($pdo,$userId);
    if(!$o||$o['status']!=='approved')return false;
    $age=afResumoIdade($pdo,$userId);
    if(!$age['completo']||(int)($age['idade']??0)<18)return false;
    if(function_exists('afAgeAssuranceStatus')){try{if(empty(afAgeAssuranceStatus($pdo,$userId)['ready']))return false;}catch(Throwable){return false;}}
    return true;
}

function afControlledEventRequestOrganizer(PDO $pdo,int $userId,string $organizationName,string $phone=''): array
{
    $age=afResumoIdade($pdo,$userId);
    if(!$age['completo']||(int)($age['idade']??0)<18)throw new RuntimeException('Somente uma conta adulta confirmada pode solicitar acesso de organizador.');
    $organizationName=substr(trim(strip_tags($organizationName)),0,180);$phone=substr(trim(strip_tags($phone)),0,30);
    if($organizationName==='')throw new RuntimeException('Informe o nome da organização ou responsável pelo evento.');
    $pdo->prepare("INSERT INTO af_controlled_event_organizers(usuario_id,organization_name,contact_phone,status,created_at,updated_at) VALUES (?,?,?,'pending',NOW(),NOW()) ON DUPLICATE KEY UPDATE organization_name=VALUES(organization_name),contact_phone=VALUES(contact_phone),status=IF(status='approved','approved','pending'),updated_at=NOW()")
        ->execute([$userId,$organizationName,$phone?:null]);
    return afControlledEventOrganizer($pdo,$userId)?:[];
}

function afControlledEventAdminSetOrganizer(PDO $pdo,int $organizerId,string $status,int $adminId,string $reason=''): void
{
    if(!in_array($status,['approved','suspended','revoked'],true))throw new RuntimeException('Status de organizador inválido.');
    if($status==='approved'){
        $pdo->prepare("UPDATE af_controlled_event_organizers SET status='approved',approved_by_admin_id=?,approved_at=NOW(),suspension_reason=NULL,updated_at=NOW() WHERE id=?")->execute([$adminId,$organizerId]);
    }else{
        $pdo->prepare('UPDATE af_controlled_event_organizers SET status=?,suspension_reason=?,updated_at=NOW() WHERE id=?')->execute([$status,$reason?:null,$organizerId]);
    }
}

function afControlledEventById(PDO $pdo,int $eventId,bool $forUpdate=false): ?array
{
    $sql='SELECT e.*,o.usuario_id organizer_user_id,o.organization_name,o.status organizer_status,u.nome organizer_name,u.email organizer_email FROM af_controlled_events e JOIN af_controlled_event_organizers o ON o.id=e.organizer_id JOIN af_usuarios u ON u.id=o.usuario_id WHERE e.id=? LIMIT 1'.($forUpdate?' FOR UPDATE':'');
    $st=$pdo->prepare($sql);$st->execute([$eventId]);return $st->fetch()?:null;
}

function afControlledEventByCode(PDO $pdo,string $code): ?array
{
    $code=strtoupper(preg_replace('/[^A-Z0-9]/','',$code));
    $st=$pdo->prepare('SELECT e.*,o.usuario_id organizer_user_id,o.organization_name,u.nome organizer_name FROM af_controlled_events e JOIN af_controlled_event_organizers o ON o.id=e.organizer_id JOIN af_usuarios u ON u.id=o.usuario_id WHERE e.public_code=? LIMIT 1');$st->execute([$code]);return $st->fetch()?:null;
}

function afControlledEventAssertOrganizerOwns(PDO $pdo,int $eventId,int $userId): array
{
    $e=afControlledEventById($pdo,$eventId);
    if(!$e||(int)$e['organizer_user_id']!==$userId)throw new RuntimeException('Evento não encontrado para esta conta de organizador.');
    if(!afControlledEventOrganizerApproved($pdo,$userId))throw new RuntimeException('A conta de organizador não está aprovada.');
    return $e;
}

function afControlledEventParseDate(string $value,string $label): string
{
    $value=trim($value);$ts=strtotime($value);if(!$ts)throw new RuntimeException('Informe '.$label.' válida.');return date('Y-m-d H:i:s',$ts);
}

function afControlledEventCreate(PDO $pdo,int $organizerUserId,array $data): array
{
    if(!afControlledEventEnabled($pdo))throw new RuntimeException('O modo evento infantil está desativado.');
    $organizer=afControlledEventOrganizer($pdo,$organizerUserId);if(!$organizer||$organizer['status']!=='approved')throw new RuntimeException('Sua conta de organizador precisa ser aprovada pelo administrador.');
    $name=substr(trim(strip_tags((string)($data['name']??''))),0,180);$venue=substr(trim(strip_tags((string)($data['venue_name']??''))),0,180);
    $camera=trim(strip_tags((string)($data['camera_area_instructions']??'')));$safety=trim(strip_tags((string)($data['safety_plan']??'')));
    $emergencyName=substr(trim(strip_tags((string)($data['emergency_contact_name']??''))),0,180);$emergencyPhone=substr(trim(strip_tags((string)($data['emergency_contact_phone']??''))),0,30);
    if($name===''||$venue===''||$camera===''||$safety===''||$emergencyName===''||$emergencyPhone==='')throw new RuntimeException('Preencha nome, local, área de câmera, plano de segurança e contato de emergência.');
    $start=afControlledEventParseDate((string)($data['starts_at']??''),'a data de início');$end=afControlledEventParseDate((string)($data['ends_at']??''),'a data de término');
    if(strtotime($start)<time()+900)throw new RuntimeException('O evento precisa ser criado com pelo menos 15 minutos de antecedência.');
    if(strtotime($end)<=strtotime($start))throw new RuntimeException('O término precisa ocorrer depois do início.');
    $window=max(15,min(240,(int)afControlledEventConfig($pdo,'controlled_event_checkin_window_minutes',90)));
    $open=date('Y-m-d H:i:s',strtotime($start)-$window*60);$close=$end;
    $minAge=max(0,min(15,(int)($data['min_age']??0)));$maxAge=max($minAge,min(15,(int)($data['max_age']??15)));
    $capacity=max(1,min(500,(int)($data['capacity']??50)));
    $maxRetention=max(1,min(365,(int)afControlledEventConfig($pdo,'controlled_event_max_retention_days',90)));
    $retention=max(1,min($maxRetention,(int)($data['retention_days']??afControlledEventConfig($pdo,'controlled_event_default_retention_days',30))));
    $purge=date('Y-m-d H:i:s',strtotime($end)+$retention*86400);
    $code=afControlledEventRandomCode($pdo,'af_controlled_events','public_code',6);
    $pdo->prepare("INSERT INTO af_controlled_events(organizer_id,public_code,name,description,venue_name,venue_address_private,camera_area_instructions,safety_plan,emergency_contact_name,emergency_contact_phone,starts_at,ends_at,checkin_opens_at,checkin_closes_at,min_age,max_age,capacity,retention_days,purge_after,status,public_stream_enabled,comments_enabled,messages_enabled,public_profile_enabled,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'draft',0,0,0,0,NOW(),NOW())")
        ->execute([(int)$organizer['id'],$code,$name,substr(trim(strip_tags((string)($data['description']??''))),0,3000)?:null,$venue,substr(trim(strip_tags((string)($data['venue_address_private']??''))),0,500)?:null,$camera,$safety,$emergencyName,$emergencyPhone,$start,$end,$open,$close,$minAge,$maxAge,$capacity,$retention,$purge]);
    return afControlledEventById($pdo,(int)$pdo->lastInsertId())?:[];
}

function afControlledEventUpdateStatusByOrganizer(PDO $pdo,int $eventId,int $userId,string $action): void
{
    $e=afControlledEventAssertOrganizerOwns($pdo,$eventId,$userId);
    if($action==='submit'&&$e['status']==='draft'){$pdo->prepare("UPDATE af_controlled_events SET status='pending_approval',updated_at=NOW() WHERE id=?")->execute([$eventId]);return;}
    if($action==='cancel'&&in_array($e['status'],['draft','pending_approval','approved','active'],true)){
        $pdo->prepare("UPDATE af_controlled_events SET status='cancelled',cancelled_at=NOW(),cancellation_reason='Cancelado pelo organizador',updated_at=NOW() WHERE id=?")->execute([$eventId]);
        $pdo->prepare("UPDATE af_controlled_event_qr_tokens q JOIN af_controlled_event_participants p ON p.id=q.participant_id SET q.status='revoked',q.updated_at=NOW() WHERE p.event_id=? AND q.status IN ('active','used')")->execute([$eventId]);
        $pdo->prepare("UPDATE af_controlled_event_supervision_sessions SET status='closed',ended_at=NOW(),updated_at=NOW() WHERE event_id=? AND status='active'")->execute([$eventId]);return;
    }
    if($action==='activate'&&$e['status']==='approved'){
        if(strtotime((string)$e['ends_at'])<=time())throw new RuntimeException('Este evento já terminou.');
        $pdo->prepare("UPDATE af_controlled_events SET status='active',activated_at=NOW(),updated_at=NOW() WHERE id=?")->execute([$eventId]);
        afControlledEventStartSupervision($pdo,$eventId,$userId);return;
    }
    if($action==='finish'&&$e['status']==='active'){$pdo->prepare("UPDATE af_controlled_events SET status='finished',finished_at=NOW(),updated_at=NOW() WHERE id=?")->execute([$eventId]);$pdo->prepare("UPDATE af_controlled_event_supervision_sessions SET status='closed',ended_at=NOW(),updated_at=NOW() WHERE event_id=? AND status='active'")->execute([$eventId]);return;}
    throw new RuntimeException('Ação indisponível para o estado atual do evento.');
}

function afControlledEventAdminSetEventStatus(PDO $pdo,int $eventId,string $status,int $adminId,string $reason=''): void
{
    if(!in_array($status,['approved','cancelled'],true))throw new RuntimeException('Ação administrativa inválida.');
    if($status==='approved'){
        $event=afControlledEventById($pdo,$eventId);if(!$event)throw new RuntimeException('Evento não encontrado.');
        if(!afControlledEventOrganizerApproved($pdo,(int)$event['organizer_user_id']))throw new RuntimeException('O organizador precisa estar aprovado e com maioridade confirmada.');
        if(function_exists('afPrivacyMinorReleaseReady')){$release=afPrivacyMinorReleaseReady($pdo);if(empty($release['ready']))throw new RuntimeException('O relatório de riscos, os dados do controlador e a revisão jurídica precisam estar válidos antes da aprovação.');}
        $pdo->prepare("UPDATE af_controlled_events SET status='approved',approved_by_admin_id=?,approved_at=NOW(),cancellation_reason=NULL,updated_at=NOW() WHERE id=? AND status IN ('pending_approval','draft')")->execute([$adminId,$eventId]);
    }else{$pdo->prepare("UPDATE af_controlled_events SET status='cancelled',cancelled_at=NOW(),cancellation_reason=?,updated_at=NOW() WHERE id=? AND status<>'purged'")->execute([$reason?:'Cancelado pela administração',$eventId]);}
}

function afControlledEventListForOrganizer(PDO $pdo,int $userId): array
{
    $st=$pdo->prepare('SELECT e.*,(SELECT COUNT(*) FROM af_controlled_event_participants p WHERE p.event_id=e.id AND p.status<>\'purged\') participant_count,(SELECT COUNT(*) FROM af_controlled_event_participants p WHERE p.event_id=e.id AND p.status IN (\'authorized\',\'checked_in\',\'participating\',\'completed\')) authorized_count FROM af_controlled_events e JOIN af_controlled_event_organizers o ON o.id=e.organizer_id WHERE o.usuario_id=? ORDER BY e.starts_at DESC,e.id DESC');$st->execute([$userId]);return $st->fetchAll()?:[];
}

function afControlledEventActiveGuardianLink(PDO $pdo,int $minorUserId): array
{
    $link=afGuardianBuscarVinculoDoMenor($pdo,$minorUserId);
    if(!$link||!in_array((string)$link['status'],['verified_pending_consent','active'],true)||(string)($link['guardian_status']??'')!=='verified'||empty($link['family_confirmed_at']))throw new RuntimeException('O participante precisa ter responsável verificado e vínculo familiar confirmado.');
    if(function_exists('afGuardianAdultVerified')&&!afGuardianAdultVerified($pdo,(int)$link['guardian_usuario_id']))throw new RuntimeException('A verificação do responsável não está válida.');
    return $link;
}

function afControlledEventAddParticipant(PDO $pdo,int $eventId,int $organizerUserId,string $identifier): array
{
    $e=afControlledEventAssertOrganizerOwns($pdo,$eventId,$organizerUserId);
    if(!in_array($e['status'],['draft','pending_approval','approved'],true))throw new RuntimeException('A lista não pode ser alterada após o início do evento.');
    $identifier=trim($identifier);if($identifier==='')throw new RuntimeException('Informe o Aura ID ou e-mail do participante.');
    $st=$pdo->prepare('SELECT u.*,a.data_nascimento,a.idade_declarada,a.tipo_conta,a.status_idade FROM af_usuarios u LEFT JOIN af_user_age_profiles a ON a.usuario_id=u.id WHERE UPPER(u.aura_id)=UPPER(?) OR LOWER(u.email)=LOWER(?) LIMIT 1');$st->execute([$identifier,$identifier]);$minor=$st->fetch();if(!$minor)throw new RuntimeException('Conta do participante não encontrada.');
    $age=afResumoIdade($pdo,(int)$minor['id']);$ageValue=(int)($age['idade']??-1);
    if(!$age['completo']||($age['tipo_conta']??'')!=='minor'||$ageValue<0||$ageValue>=16)throw new RuntimeException('O modo infantil aceita somente contas menores de 16 anos com idade confirmada.');
    if($ageValue<(int)$e['min_age']||$ageValue>(int)$e['max_age'])throw new RuntimeException('A idade do participante está fora da faixa definida para o evento.');
    $st=$pdo->prepare("SELECT COUNT(*) FROM af_controlled_event_participants WHERE event_id=? AND status NOT IN ('removed','cancelled','purged')");$st->execute([$eventId]);if((int)$st->fetchColumn()>=(int)$e['capacity'])throw new RuntimeException('O evento atingiu a capacidade cadastrada.');
    $link=afControlledEventActiveGuardianLink($pdo,(int)$minor['id']);$alias=substr((string)($minor['username']?:'Participante protegido'),0,80);
    $pdo->prepare("INSERT INTO af_controlled_event_participants(event_id,minor_user_id,guardian_link_id,alias_snapshot,status,added_by_user_id,created_at,updated_at) VALUES (?,?,?,?, 'invited',?,NOW(),NOW()) ON DUPLICATE KEY UPDATE guardian_link_id=VALUES(guardian_link_id),alias_snapshot=VALUES(alias_snapshot),status=IF(status IN ('removed','cancelled'),'invited',status),removed_at=NULL,removal_reason=NULL,updated_at=NOW()")
        ->execute([$eventId,(int)$minor['id'],(int)$link['id'],$alias,$organizerUserId]);
    $st=$pdo->prepare('SELECT p.*,u.nome minor_name,u.aura_id,u.username,g.nome guardian_name,g.email guardian_email FROM af_controlled_event_participants p JOIN af_usuarios u ON u.id=p.minor_user_id JOIN af_guardian_minor_links l ON l.id=p.guardian_link_id JOIN af_usuarios g ON g.id=l.guardian_usuario_id WHERE p.event_id=? AND p.minor_user_id=? LIMIT 1');$st->execute([$eventId,(int)$minor['id']]);$p=$st->fetch()?:[];
    criarNotificacao($pdo,(int)$link['guardian_usuario_id'],'controlled_event_invitation','Autorização de evento pendente','Revise e autorize a participação de '.$minor['nome'].' no evento infantil '.$e['name'].'.',['event_id'=>$eventId,'participant_id'=>(int)($p['id']??0)]);
    return $p;
}

function afControlledEventRemoveParticipant(PDO $pdo,int $eventId,int $organizerUserId,int $participantId,string $reason='Removido pela organização'): void
{
    afControlledEventAssertOrganizerOwns($pdo,$eventId,$organizerUserId);
    $p=afControlledEventParticipant($pdo,$participantId);if(!$p||(int)$p['event_id']!==$eventId)throw new RuntimeException('Participante não encontrado neste evento.');
    if($p['status']==='purged')throw new RuntimeException('Os dados deste participante já foram eliminados.');
    $reason=substr(trim(strip_tags($reason))?:'Removido pela organização',0,500);
    $pdo->beginTransaction();try{
        $pdo->prepare("UPDATE af_controlled_event_consents SET status='revoked',revoked_at=NOW(),revocation_reason=?,updated_at=NOW() WHERE participant_id=? AND status='active'")->execute([$reason,$participantId]);
        $pdo->prepare("UPDATE af_controlled_event_qr_tokens SET status='revoked',updated_at=NOW() WHERE participant_id=? AND status IN ('active','used')")->execute([$participantId]);
        $pdo->prepare("UPDATE af_controlled_event_participants SET status='removed',removed_at=NOW(),removal_reason=?,updated_at=NOW() WHERE id=?")->execute([$reason,$participantId]);
        $pdo->commit();
    }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
    criarNotificacao($pdo,(int)$p['minor_user_id'],'controlled_event_removed','Participação cancelada','Sua participação no evento '.$p['event_name'].' foi cancelada pela organização.',['event_id'=>$eventId]);
    criarNotificacao($pdo,(int)$p['guardian_user_id'],'controlled_event_removed_guardian','Participação do dependente cancelada','A participação de '.$p['minor_name'].' no evento '.$p['event_name'].' foi cancelada.',['event_id'=>$eventId]);
}

function afControlledEventParticipant(PDO $pdo,int $participantId,bool $forUpdate=false): ?array
{
    $sql='SELECT p.*,e.name event_name,e.public_code,e.status event_status,e.starts_at,e.ends_at,e.checkin_opens_at,e.checkin_closes_at,e.camera_area_instructions,e.safety_plan,e.venue_name,e.retention_days,e.purge_after,e.min_age,e.max_age,o.usuario_id organizer_user_id,o.organization_name,u.nome minor_name,u.username minor_username,u.aura_id minor_aura_id,g.usuario_id guardian_user_id,gu.nome guardian_name FROM af_controlled_event_participants p JOIN af_controlled_events e ON e.id=p.event_id JOIN af_guardian_minor_links l ON l.id=p.guardian_link_id JOIN af_controlled_event_organizers o ON o.id=e.organizer_id JOIN af_usuarios u ON u.id=p.minor_user_id JOIN af_guardian_minor_links g ON g.id=p.guardian_link_id JOIN af_usuarios gu ON gu.id=g.guardian_usuario_id WHERE p.id=? LIMIT 1'.($forUpdate?' FOR UPDATE':'');
    $st=$pdo->prepare($sql);$st->execute([$participantId]);return $st->fetch()?:null;
}

function afControlledEventParticipants(PDO $pdo,int $eventId): array
{
    $st=$pdo->prepare('SELECT p.*,u.nome minor_name,u.username,u.aura_id,g.nome guardian_name,gr.email guardian_email,q.short_code,q.status qr_status,q.expires_at qr_expires FROM af_controlled_event_participants p JOIN af_usuarios u ON u.id=p.minor_user_id JOIN af_guardian_minor_links l ON l.id=p.guardian_link_id JOIN af_usuarios g ON g.id=l.guardian_usuario_id JOIN af_usuarios gr ON gr.id=l.guardian_usuario_id LEFT JOIN af_controlled_event_qr_tokens q ON q.participant_id=p.id AND q.status IN (\'active\',\'used\') WHERE p.event_id=? ORDER BY p.created_at ASC');$st->execute([$eventId]);return $st->fetchAll()?:[];
}

function afControlledEventConsentVersion(PDO $pdo): array
{
    $wanted=(string)afControlledEventConfig($pdo,'controlled_event_current_consent_version','1.0');
    $st=$pdo->prepare('SELECT * FROM af_controlled_event_consent_versions WHERE version_key=? AND active=1 LIMIT 1');$st->execute([$wanted]);$row=$st->fetch();
    if(!$row)throw new RuntimeException('A versão atual da autorização do evento não está publicada.');
    if(!hash_equals((string)$row['content_hash'],hash('sha256',(string)$row['content_text'])))throw new RuntimeException('A versão da autorização do evento falhou na validação de integridade.');
    return $row;
}

function afControlledEventTermsHash(array $participant,string $version,string $contentHash=''): string
{
    return hash('sha256',json_encode(['version'=>$version,'content_hash'=>$contentHash,'event_id'=>(int)$participant['event_id'],'participant_id'=>(int)$participant['id'],'event_snapshot'=>['name'=>$participant['event_name']??'','starts_at'=>$participant['starts_at']??'','ends_at'=>$participant['ends_at']??'','venue_name'=>$participant['venue_name']??'','camera_area_instructions'=>$participant['camera_area_instructions']??'','safety_plan'=>$participant['safety_plan']??'','retention_days'=>(int)($participant['retention_days']??0)],'rules'=>['account_participation'=>1,'camera_processing'=>1,'body_processing'=>1,'event_ranking'=>1,'guardian_notifications'=>1,'public_stream'=>0,'comments'=>0,'private_messages'=>0,'recording'=>0,'public_profile'=>0]],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));
}

function afControlledEventCreateQr(PDO $pdo,int $participantId,string $expiresAt): array
{
    $raw=bin2hex(random_bytes(24));$hash=hash('sha256',$raw);$prefix=substr(strtoupper($raw),0,8);$short=afControlledEventRandomCode($pdo,'af_controlled_event_qr_tokens','short_code',5);
    $pdo->prepare("UPDATE af_controlled_event_qr_tokens SET status='revoked',updated_at=NOW() WHERE participant_id=? AND status IN ('active','used')")->execute([$participantId]);
    $pdo->prepare("INSERT INTO af_controlled_event_qr_tokens(participant_id,token_hash,token_prefix,short_code,status,expires_at,created_at,updated_at) VALUES (?,?,?,?,'active',?,NOW(),NOW())")->execute([$participantId,$hash,$prefix,$short,$expiresAt]);
    return ['raw_token'=>$raw,'short_code'=>$short,'expires_at'=>$expiresAt,'id'=>(int)$pdo->lastInsertId()];
}

function afControlledEventGrantConsent(PDO $pdo,int $participantId,int $guardianUserId,array $permissions): array
{
    $p=afControlledEventParticipant($pdo,$participantId);if(!$p)throw new RuntimeException('Participação não encontrada.');
    if((int)$p['guardian_user_id']!==$guardianUserId)throw new RuntimeException('Esta autorização pertence a outro responsável.');
    if(!in_array($p['event_status'],['draft','pending_approval','approved'],true))throw new RuntimeException('O evento não aceita novas autorizações.');
    if(function_exists('afGuardianAdultVerified')&&!afGuardianAdultVerified($pdo,$guardianUserId))throw new RuntimeException('Conclua a verificação do responsável antes de autorizar o evento.');
    foreach(['account_participation','camera_processing','body_processing','event_ranking','guardian_notifications'] as $key){if(empty($permissions[$key]))throw new RuntimeException('Todas as autorizações necessárias do evento precisam ser confirmadas individualmente.');}
    $versionRow=afControlledEventConsentVersion($pdo);$version=(string)$versionRow['version_key'];$hash=afControlledEventTermsHash($p,$version,(string)$versionRow['content_hash']);
    $pdo->beginTransaction();try{
        $pdo->prepare("UPDATE af_controlled_event_consents SET status='superseded',updated_at=NOW() WHERE participant_id=? AND status='active'")->execute([$participantId]);
        $pdo->prepare("INSERT INTO af_controlled_event_consents(participant_id,guardian_user_id,consent_version_id,consent_version,account_participation,camera_processing,body_processing,event_ranking,guardian_notifications,public_stream,comments,private_messages,status,terms_hash,confirmation_method,ip_address,user_agent,device_uuid,granted_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?, ?,0,0,0,'active',?,'authenticated_guardian',?,?,?,NOW(),NOW(),NOW())")
            ->execute([$participantId,$guardianUserId,(int)$versionRow['id'],$version,1,1,1,1,1,$hash,clientIp(),substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,1000),substr(deviceUuid(),0,64)]);
        $consentId=(int)$pdo->lastInsertId();$qr=afControlledEventCreateQr($pdo,$participantId,date('Y-m-d H:i:s',strtotime((string)$p['ends_at'])+3600));
        $pdo->prepare("UPDATE af_controlled_event_participants SET consent_id=?,status='authorized',authorized_at=NOW(),updated_at=NOW() WHERE id=?")->execute([$consentId,$participantId]);
        $pdo->commit();
    }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
    criarNotificacao($pdo,(int)$p['minor_user_id'],'controlled_event_authorized','Evento infantil autorizado','Seu responsável autorizou sua participação em '.$p['event_name'].'. O QR Code será usado somente no local supervisionado.',['event_id'=>(int)$p['event_id'],'participant_id'=>$participantId]);
    return ['consent_id'=>$consentId,'qr'=>$qr];
}

function afControlledEventRevokeConsent(PDO $pdo,int $participantId,int $guardianUserId,string $reason='Revogado pelo responsável'): void
{
    $p=afControlledEventParticipant($pdo,$participantId);if(!$p||(int)$p['guardian_user_id']!==$guardianUserId)throw new RuntimeException('Autorização não encontrada.');
    $pdo->beginTransaction();try{
        $pdo->prepare("UPDATE af_controlled_event_consents SET status='revoked',revoked_at=NOW(),revocation_reason=?,updated_at=NOW() WHERE participant_id=? AND status='active'")->execute([substr($reason,0,500),$participantId]);
        $pdo->prepare("UPDATE af_controlled_event_qr_tokens SET status='revoked',updated_at=NOW() WHERE participant_id=? AND status IN ('active','used')")->execute([$participantId]);
        $pdo->prepare("UPDATE af_controlled_event_participants SET status='cancelled',updated_at=NOW() WHERE id=? AND status NOT IN ('completed','purged')")->execute([$participantId]);$pdo->commit();
    }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
}

function afControlledEventQrPayload(array $participant,string $rawToken): string
{
    return SITE_URL.'/pages/evento-checkin.php?token='.rawurlencode($rawToken).'&event='.rawurlencode((string)$participant['public_code']);
}

function afControlledEventStartSupervision(PDO $pdo,int $eventId,int $organizerUserId): array
{
    $e=afControlledEventAssertOrganizerOwns($pdo,$eventId,$organizerUserId);if($e['status']!=='active')throw new RuntimeException('A supervisão só pode ser ativada durante um evento ativo.');
    $pdo->prepare("UPDATE af_controlled_event_supervision_sessions SET status='closed',ended_at=NOW(),updated_at=NOW() WHERE event_id=? AND organizer_user_id=? AND status='active'")->execute([$eventId,$organizerUserId]);
    $pdo->prepare("INSERT INTO af_controlled_event_supervision_sessions(event_id,organizer_user_id,status,started_at,last_heartbeat_at,ip_address,device_uuid,created_at,updated_at) VALUES (?,?,'active',NOW(),NOW(),?,?,NOW(),NOW())")->execute([$eventId,$organizerUserId,clientIp(),substr(deviceUuid(),0,64)]);
    return ['id'=>(int)$pdo->lastInsertId(),'event_id'=>$eventId,'active'=>true];
}

function afControlledEventHeartbeat(PDO $pdo,int $eventId,int $organizerUserId): array
{
    afControlledEventAssertOrganizerOwns($pdo,$eventId,$organizerUserId);
    $st=$pdo->prepare("UPDATE af_controlled_event_supervision_sessions SET last_heartbeat_at=NOW(),updated_at=NOW() WHERE event_id=? AND organizer_user_id=? AND status='active' ORDER BY id DESC LIMIT 1");$st->execute([$eventId,$organizerUserId]);
    if(!$st->rowCount())return afControlledEventStartSupervision($pdo,$eventId,$organizerUserId);
    return ['event_id'=>$eventId,'active'=>true,'heartbeat_at'=>date(DATE_ATOM)];
}

function afControlledEventHasActiveSupervision(PDO $pdo,int $eventId): bool
{
    $timeout=max(60,min(900,(int)afControlledEventConfig($pdo,'controlled_event_supervision_timeout_seconds',150)));
    $pdo->prepare("UPDATE af_controlled_event_supervision_sessions SET status='expired',ended_at=NOW(),updated_at=NOW() WHERE event_id=? AND status='active' AND last_heartbeat_at<DATE_SUB(NOW(),INTERVAL {$timeout} SECOND)")->execute([$eventId]);
    $st=$pdo->prepare("SELECT id FROM af_controlled_event_supervision_sessions WHERE event_id=? AND status='active' AND last_heartbeat_at>=DATE_SUB(NOW(),INTERVAL {$timeout} SECOND) LIMIT 1");$st->execute([$eventId]);return (bool)$st->fetchColumn();
}

function afControlledEventCheckin(PDO $pdo,int $organizerUserId,string $rawToken='',string $shortCode='',bool $cameraZone=true): array
{
    afRateLimitOrFail($pdo,'controlled_event_checkin',$organizerUserId.'|'.clientIp(),30,300,600);
    $rawToken=strtolower(preg_replace('/[^a-f0-9]/','',$rawToken));$shortCode=strtoupper(preg_replace('/[^A-Z0-9]/','',$shortCode));
    if(strlen($rawToken)!==48&&strlen($shortCode)!==10)throw new RuntimeException('QR Code ou código manual inválido.');
    $where=strlen($rawToken)===48?'q.token_hash=?':'q.short_code=?';$value=strlen($rawToken)===48?hash('sha256',$rawToken):$shortCode;
    $st=$pdo->prepare("SELECT q.*,p.event_id,p.minor_user_id,l.guardian_usuario_id guardian_user_id,p.status participant_status,p.consent_id,e.status event_status,e.checkin_opens_at,e.checkin_closes_at,e.name event_name,o.usuario_id organizer_user_id FROM af_controlled_event_qr_tokens q JOIN af_controlled_event_participants p ON p.id=q.participant_id JOIN af_controlled_events e ON e.id=p.event_id JOIN af_guardian_minor_links l ON l.id=p.guardian_link_id JOIN af_controlled_event_organizers o ON o.id=e.organizer_id WHERE {$where} LIMIT 1");$st->execute([$value]);$row=$st->fetch();if(!$row)throw new RuntimeException('Credencial do participante não encontrada.');
    if((int)$row['organizer_user_id']!==$organizerUserId)throw new RuntimeException('Esta credencial pertence a outro evento.');
    if(!in_array($row['event_status'],['approved','active'],true))throw new RuntimeException('O evento ainda não está disponível para check-in.');
    if(!in_array($row['status'],['active','used'],true)||strtotime((string)$row['expires_at'])<=time())throw new RuntimeException('Esta credencial expirou ou foi revogada.');
    if(!in_array($row['participant_status'],['authorized','checked_in','participating'],true)||empty($row['consent_id']))throw new RuntimeException('A autorização do responsável não está ativa.');
    if(time()<strtotime((string)$row['checkin_opens_at'])||time()>strtotime((string)$row['checkin_closes_at']))throw new RuntimeException('O check-in está fora da janela permitida.');
    if(!afControlledEventHasActiveSupervision($pdo,(int)$row['event_id']))throw new RuntimeException('Ative a supervisão presencial antes do check-in.');
    $pdo->beginTransaction();try{
        $pdo->prepare("UPDATE af_controlled_event_qr_tokens SET status='used',first_used_at=COALESCE(first_used_at,NOW()),last_used_at=NOW(),use_count=use_count+1,updated_at=NOW() WHERE id=?")->execute([(int)$row['id']]);
        $pdo->prepare("INSERT INTO af_controlled_event_checkins(participant_id,qr_token_id,organizer_user_id,checkin_type,camera_zone_confirmed,details_json,ip_address,device_uuid,created_at) VALUES (?,?,?,'camera_zone',?,NULL,?,?,NOW())")
            ->execute([(int)$row['participant_id'],(int)$row['id'],$organizerUserId,$cameraZone?1:0,clientIp(),substr(deviceUuid(),0,64)]);
        $pdo->prepare("UPDATE af_controlled_event_participants SET status='checked_in',checked_in_at=COALESCE(checked_in_at,NOW()),camera_zone_confirmed_at=IF(?,NOW(),camera_zone_confirmed_at),updated_at=NOW() WHERE id=?")
            ->execute([$cameraZone?1:0,(int)$row['participant_id']]);$pdo->commit();
    }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
    criarNotificacao($pdo,(int)$row['minor_user_id'],'controlled_event_checkin','Check-in confirmado','Seu acesso à área supervisionada do evento '.$row['event_name'].' foi confirmado.',['event_id'=>(int)$row['event_id']]);
    criarNotificacao($pdo,(int)$row['guardian_user_id'],'controlled_event_checkin_guardian','Check-in do dependente confirmado','O check-in presencial no evento '.$row['event_name'].' foi confirmado pela organização.',['event_id'=>(int)$row['event_id'],'minor_user_id'=>(int)$row['minor_user_id']]);
    return afControlledEventParticipant($pdo,(int)$row['participant_id'])?:[];
}

function afControlledEventAccessDecision(PDO $pdo,int $minorUserId,int $eventId,bool $requireCheckin=true): array
{
    if(!afControlledEventEnabled($pdo))return ['allowed'=>false,'code'=>'CONTROLLED_EVENT_DISABLED','message'=>'O modo evento infantil está desativado.'];
    $st=$pdo->prepare("SELECT p.*,e.status event_status,e.starts_at,e.ends_at,e.purge_after,e.public_stream_enabled,e.comments_enabled,e.messages_enabled,c.status consent_status,c.consent_version,c.account_participation,c.camera_processing,c.body_processing,c.event_ranking,c.public_stream,c.comments,c.private_messages FROM af_controlled_event_participants p JOIN af_controlled_events e ON e.id=p.event_id LEFT JOIN af_controlled_event_consents c ON c.id=p.consent_id WHERE p.event_id=? AND p.minor_user_id=? LIMIT 1");$st->execute([$eventId,$minorUserId]);$p=$st->fetch();
    if(!$p)return ['allowed'=>false,'code'=>'EVENT_PARTICIPANT_NOT_LISTED','message'=>'A conta não está na lista antecipada deste evento.'];
    if($p['event_status']!=='active')return ['allowed'=>false,'code'=>'EVENT_NOT_ACTIVE','message'=>'O evento ainda não está ativo ou foi encerrado.'];
    if(time()<strtotime((string)$p['starts_at'])-900||time()>strtotime((string)$p['ends_at'])+900)return ['allowed'=>false,'code'=>'EVENT_OUTSIDE_SCHEDULE','message'=>'A participação está fora do horário controlado do evento.'];
    if(!in_array($p['status'],['authorized','checked_in','participating','completed'],true)||$p['consent_status']!=='active')return ['allowed'=>false,'code'=>'EVENT_GUARDIAN_AUTH_REQUIRED','message'=>'A autorização específica do responsável ainda não está ativa.'];
    $currentVersion=(string)afControlledEventConfig($pdo,'controlled_event_current_consent_version','1.0');if((string)$p['consent_version']!==$currentVersion)return ['allowed'=>false,'code'=>'EVENT_CONSENT_UPDATE_REQUIRED','message'=>'O responsável precisa revisar a versão atual da autorização do evento.'];
    foreach(['account_participation','camera_processing','body_processing','event_ranking'] as $key)if(empty($p[$key]))return ['allowed'=>false,'code'=>'EVENT_PERMISSION_REQUIRED','message'=>'Uma autorização obrigatória do evento não foi concedida.'];
    if(!empty($p['public_stream'])||!empty($p['comments'])||!empty($p['private_messages'])||!empty($p['public_stream_enabled'])||!empty($p['comments_enabled'])||!empty($p['messages_enabled']))return ['allowed'=>false,'code'=>'EVENT_UNSAFE_CONFIGURATION','message'=>'O evento possui configuração social incompatível com o modo infantil.'];
    if($requireCheckin&&(empty($p['checked_in_at'])||empty($p['camera_zone_confirmed_at'])))return ['allowed'=>false,'code'=>'EVENT_CHECKIN_REQUIRED','message'=>'Faça o check-in por QR Code na área delimitada de câmera.'];
    if($requireCheckin&&!afControlledEventHasActiveSupervision($pdo,$eventId))return ['allowed'=>false,'code'=>'EVENT_SUPERVISION_REQUIRED','message'=>'A sessão foi pausada porque não há supervisão presencial ativa.'];
    return ['allowed'=>true,'code'=>'CONTROLLED_EVENT_ALLOWED','message'=>'Participação autorizada dentro do evento supervisionado.','participant'=>$p,'event_id'=>$eventId];
}

function afControlledEventAssertAccess(PDO $pdo,int $minorUserId,int $eventId,bool $requireCheckin=true): array
{
    $d=afControlledEventAccessDecision($pdo,$minorUserId,$eventId,$requireCheckin);if(empty($d['allowed']))throw new RuntimeException((string)$d['message']);return $d;
}

function afControlledEventListForMinor(PDO $pdo,int $minorUserId): array
{
    $st=$pdo->prepare("SELECT p.*,e.name,e.public_code,e.venue_name,e.starts_at,e.ends_at,e.camera_area_instructions,e.status event_status,e.retention_days,e.purge_after,q.short_code,q.status qr_status,q.expires_at qr_expires FROM af_controlled_event_participants p JOIN af_controlled_events e ON e.id=p.event_id LEFT JOIN af_controlled_event_qr_tokens q ON q.participant_id=p.id AND q.status IN ('active','used') WHERE p.minor_user_id=? AND p.status<>'purged' ORDER BY e.starts_at DESC");$st->execute([$minorUserId]);return $st->fetchAll()?:[];
}

function afControlledEventListForGuardian(PDO $pdo,int $guardianUserId): array
{
    $st=$pdo->prepare("SELECT p.*,e.name,e.public_code,e.venue_name,e.starts_at,e.ends_at,e.camera_area_instructions,e.status event_status,e.retention_days,e.purge_after,u.nome minor_name,u.username minor_username,u.aura_id minor_aura_id,c.status consent_status,c.consent_version,q.short_code,q.status qr_status,q.expires_at qr_expires FROM af_controlled_event_participants p JOIN af_controlled_events e ON e.id=p.event_id JOIN af_guardian_minor_links l ON l.id=p.guardian_link_id JOIN af_usuarios u ON u.id=p.minor_user_id LEFT JOIN af_controlled_event_consents c ON c.id=p.consent_id LEFT JOIN af_controlled_event_qr_tokens q ON q.participant_id=p.id AND q.status IN ('active','used') WHERE l.guardian_usuario_id=? AND p.status<>'purged' ORDER BY e.starts_at DESC");$st->execute([$guardianUserId]);return $st->fetchAll()?:[];
}

function afControlledEventParticipantQr(PDO $pdo,int $participantId,int $guardianOrMinorUserId): ?array
{
    $p=afControlledEventParticipant($pdo,$participantId);if(!$p||!in_array($guardianOrMinorUserId,[(int)$p['guardian_user_id'],(int)$p['minor_user_id']],true))return null;
    $st=$pdo->prepare("SELECT * FROM af_controlled_event_qr_tokens WHERE participant_id=? AND status IN ('active','used') AND expires_at>NOW() ORDER BY id DESC LIMIT 1");$st->execute([$participantId]);$q=$st->fetch();if(!$q)return null;
    return ['participant'=>$p,'short_code'=>$q['short_code'],'token_prefix'=>$q['token_prefix'],'expires_at'=>$q['expires_at']];
}

function afControlledEventRecordResult(PDO $pdo,array $match): void
{
    $eventId=(int)($match['controlled_event_id']??0);if($eventId<=0)return;
    $d=afControlledEventAccessDecision($pdo,(int)$match['usuario_id'],$eventId,false);if(empty($d['allowed']))return;$p=$d['participant'];
    $status=($match['antifraude_status']??'')==='aprovada'?'valid':'review';
    $pdo->prepare("INSERT INTO af_controlled_event_results(event_id,participant_id,partida_id,score,status,created_at,updated_at) VALUES (?,?,?,?,?,NOW(),NOW()) ON DUPLICATE KEY UPDATE score=VALUES(score),status=VALUES(status),updated_at=NOW()")
        ->execute([$eventId,(int)$p['id'],(int)$match['id'],(float)($match['pontuacao_validada']??0),$status]);
    $pdo->prepare("UPDATE af_controlled_event_participants SET status='completed',completed_at=NOW(),updated_at=NOW() WHERE id=?")->execute([(int)$p['id']]);
    try{$full=afControlledEventParticipant($pdo,(int)$p['id']);if($full)criarNotificacao($pdo,(int)$full['guardian_user_id'],'controlled_event_result_guardian','Resultado do evento disponível','A participação de '.$full['minor_name'].' foi concluída no evento '.$full['event_name'].'. Consulte o ranking fechado.',['event_id'=>$eventId,'participant_id'=>(int)$p['id']]);}catch(Throwable){}
    $rows=$pdo->prepare("SELECT id FROM af_controlled_event_results WHERE event_id=? AND status='valid' ORDER BY score DESC,created_at ASC,id ASC");$rows->execute([$eventId]);$rank=0;foreach($rows->fetchAll(PDO::FETCH_COLUMN) as $id){$rank++;$pdo->prepare('UPDATE af_controlled_event_results SET rank_position=? WHERE id=?')->execute([$rank,(int)$id]);}
}

function afControlledEventRanking(PDO $pdo,int $eventId,int $viewerUserId): array
{
    $e=afControlledEventById($pdo,$eventId);if(!$e)throw new RuntimeException('Evento não encontrado.');
    $allowed=(int)$e['organizer_user_id']===$viewerUserId;
    if(!$allowed){$st=$pdo->prepare('SELECT p.id FROM af_controlled_event_participants p JOIN af_guardian_minor_links l ON l.id=p.guardian_link_id WHERE p.event_id=? AND (p.minor_user_id=? OR l.guardian_usuario_id=?) LIMIT 1');$st->execute([$eventId,$viewerUserId,$viewerUserId]);$allowed=(bool)$st->fetchColumn();}
    if(!$allowed)throw new RuntimeException('Ranking restrito aos participantes, responsáveis e organização do evento.');
    $st=$pdo->prepare("SELECT r.rank_position,r.score,r.status,p.alias_snapshot FROM af_controlled_event_results r JOIN af_controlled_event_participants p ON p.id=r.participant_id WHERE r.event_id=? AND r.status IN ('valid','review') ORDER BY CASE WHEN r.status='valid' THEN 0 ELSE 1 END,r.rank_position ASC,r.score DESC");$st->execute([$eventId]);return ['event'=>$e,'items'=>$st->fetchAll()?:[]];
}

function afControlledEventIsMatch(PDO $pdo,int|array $match): bool
{
    if(is_array($match))return (int)($match['controlled_event_id']??0)>0;
    $st=$pdo->prepare('SELECT controlled_event_id FROM af_partidas WHERE id=? LIMIT 1');$st->execute([$match]);return (int)$st->fetchColumn()>0;
}

function afControlledEventAssertMatchSupervision(PDO $pdo,array $match): void
{
    $eventId=(int)($match['controlled_event_id']??0);if($eventId<=0)return;
    afControlledEventAssertAccess($pdo,(int)$match['usuario_id'],$eventId,true);
}

function afControlledEventPurge(PDO $pdo,int $eventId,string $mode='automatic',int $adminId=0): array
{
    $e=afControlledEventById($pdo,$eventId);if(!$e)throw new RuntimeException('Evento não encontrado.');
    if($mode==='automatic'&&strtotime((string)$e['purge_after'])>time())throw new RuntimeException('O prazo de retenção ainda não terminou.');
    $pdo->prepare("INSERT INTO af_controlled_event_purge_logs(event_id,execution_mode,executed_by_admin_id,status,created_at) VALUES (?,?,?,'started',NOW())")->execute([$eventId,$mode,$adminId?:null]);$logId=(int)$pdo->lastInsertId();
    $counts=[];$pdo->beginTransaction();try{
        $st=$pdo->prepare('SELECT id FROM af_partidas WHERE controlled_event_id=?');$st->execute([$eventId]);$matchIds=array_map('intval',$st->fetchAll(PDO::FETCH_COLUMN)?:[]);
        if($matchIds){$marks=implode(',',array_fill(0,count($matchIds),'?'));foreach(['af_safe_camera_events','af_safe_camera_sessions','af_third_party_detections','af_live_comments','af_live_spectator_restrictions','af_partida_eventos','af_partida_espectadores','af_partida_interacoes','af_partida_combo_sequencias'] as $table){try{$s=$pdo->prepare("DELETE FROM `{$table}` WHERE partida_id IN ({$marks})");$s->execute($matchIds);$counts[$table]=$s->rowCount();}catch(Throwable){$counts[$table]=0;}}}
        $st=$pdo->prepare('DELETE FROM af_partidas WHERE controlled_event_id=?');$st->execute([$eventId]);$counts['af_partidas']=$st->rowCount();
        foreach(['af_controlled_event_results','af_controlled_event_checkins','af_controlled_event_qr_tokens','af_controlled_event_consents'] as $table){$st=$pdo->prepare("DELETE FROM {$table} WHERE ".($table==='af_controlled_event_results'?'event_id=?':'participant_id IN (SELECT id FROM af_controlled_event_participants WHERE event_id=?)'));$st->execute([$eventId]);$counts[$table]=$st->rowCount();}
        $st=$pdo->prepare('DELETE FROM af_controlled_event_participants WHERE event_id=?');$st->execute([$eventId]);$counts['participants_deleted']=$st->rowCount();
        $st=$pdo->prepare('DELETE FROM af_controlled_event_supervision_sessions WHERE event_id=?');$st->execute([$eventId]);$counts['supervision_sessions']=$st->rowCount();
        $pdo->prepare("UPDATE af_controlled_events SET status='purged',venue_address_private=NULL,emergency_contact_name='Removido após retenção',emergency_contact_phone='REMOVIDO',safety_plan='Dados operacionais removidos após o prazo de retenção.',camera_area_instructions='Dados operacionais removidos após o prazo de retenção.',updated_at=NOW() WHERE id=?")->execute([$eventId]);
        $pdo->commit();$pdo->prepare("UPDATE af_controlled_event_purge_logs SET status='completed',counts_json=? WHERE id=?")->execute([json_encode($counts,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),$logId]);return $counts;
    }catch(Throwable $ex){if($pdo->inTransaction())$pdo->rollBack();$pdo->prepare("UPDATE af_controlled_event_purge_logs SET status='failed',error_message=? WHERE id=?")->execute([substr($ex->getMessage(),0,1000),$logId]);throw $ex;}
}

function afControlledEventProcessRetention(PDO $pdo,int $limit=10,string $mode='automatic',int $adminId=0): array
{
    $limit=max(1,min(50,$limit));
    $pdo->exec("UPDATE af_controlled_events SET status='finished',finished_at=COALESCE(finished_at,ends_at),updated_at=NOW() WHERE status IN ('approved','active') AND ends_at<NOW()");
    $pdo->exec("UPDATE af_controlled_events SET status='cancelled',cancelled_at=COALESCE(cancelled_at,ends_at),cancellation_reason=COALESCE(cancellation_reason,'Evento não ativado dentro do período'),updated_at=NOW() WHERE status IN ('draft','pending_approval') AND ends_at<NOW()");
    $pdo->exec("UPDATE af_controlled_event_supervision_sessions s JOIN af_controlled_events e ON e.id=s.event_id SET s.status='closed',s.ended_at=COALESCE(s.ended_at,NOW()),s.updated_at=NOW() WHERE s.status='active' AND e.status IN ('finished','cancelled','purged')");
    $st=$pdo->query("SELECT id FROM af_controlled_events WHERE status IN ('finished','cancelled') AND purge_after<=NOW() ORDER BY purge_after ASC LIMIT {$limit}");$done=[];$errors=[];
    foreach($st->fetchAll(PDO::FETCH_COLUMN)?:[] as $id){try{$done[(int)$id]=afControlledEventPurge($pdo,(int)$id,$mode,$adminId);}catch(Throwable $e){$errors[(int)$id]=$e->getMessage();}}
    return ['processed'=>count($done),'events'=>$done,'errors'=>$errors];
}

function afControlledEventMaybeProcessRetention(PDO $pdo): void
{
    try{$last=(int)afControlledEventConfig($pdo,'controlled_event_retention_last_run',0);if($last>time()-21600)return;$pdo->prepare("UPDATE af_configuracoes SET valor=?,atualizado_em=NOW() WHERE chave='controlled_event_retention_last_run'")->execute([(string)time()]);afControlledEventProcessRetention($pdo,5,'automatic',0);}catch(Throwable $e){if(defined('APP_DEBUG')&&APP_DEBUG)error_log('[AURA-EVENT-RETENTION] '.$e->getMessage());}
}
