<?php
/**
 * AURA FARMING — Etapa 21
 * Avisos legais, aceite versionado e exercício de direitos do titular.
 */

function afPrivacyTableExists(PDO $pdo, string $table): bool
{
    static $cache = [];

    // Compatível com hospedagens compartilhadas/MariaDB onde
    // "SHOW TABLES LIKE ?" preparado pode retornar falso/erro.
    // Também evita falso positivo por cache quando houver mais de uma conexão/banco.
    $table = preg_replace('/[^A-Za-z0-9_]/', '', $table);
    if ($table === '') return false;

    $dbName = '';
    try {
        $dbName = (string)$pdo->query('SELECT DATABASE()')->fetchColumn();
    } catch (Throwable) {
        $dbName = defined('DB_NAME') ? (string)DB_NAME : '';
    }

    $cacheKey = $dbName . '.' . $table;
    if (array_key_exists($cacheKey, $cache)) return $cache[$cacheKey];

    try {
        $st = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?');
        $st->execute([$table]);
        if ((int)$st->fetchColumn() > 0) return $cache[$cacheKey] = true;
    } catch (Throwable) {
        // fallback abaixo
    }

    try {
        $rs = $pdo->query('SHOW TABLES');
        while ($row = $rs->fetch(PDO::FETCH_NUM)) {
            if (isset($row[0]) && strcasecmp((string)$row[0], $table) === 0) {
                return $cache[$cacheKey] = true;
            }
        }
    } catch (Throwable) {
        // sem acesso à listagem de tabelas
    }

    return $cache[$cacheKey] = false;
}

function afPrivacyConfig(PDO $pdo, string $key, string $default = ''): string
{
    static $cache = [];
    if (array_key_exists($key, $cache)) return $cache[$key];
    if (!afPrivacyTableExists($pdo, 'af_configuracoes')) return $cache[$key] = $default;
    try {
        $st = $pdo->prepare('SELECT valor FROM af_configuracoes WHERE chave=? LIMIT 1');
        $st->execute([$key]);
        $value = $st->fetchColumn();
        return $cache[$key] = ($value === false || $value === null || $value === '') ? $default : (string)$value;
    } catch (Throwable) {
        return $cache[$key] = $default;
    }
}

function afPrivacyController(PDO $pdo): array
{
    return [
        'name' => afPrivacyConfig($pdo, 'privacy_controller_name', SITE_NAME),
        'document' => afPrivacyConfig($pdo, 'privacy_controller_document', ''),
        'address' => afPrivacyConfig($pdo, 'privacy_controller_address', ''),
        'contact_email' => afPrivacyConfig($pdo, 'privacy_contact_email', MAIL_FROM),
        'dpo_name' => afPrivacyConfig($pdo, 'privacy_dpo_name', ''),
        'dpo_email' => afPrivacyConfig($pdo, 'privacy_dpo_email', afPrivacyConfig($pdo, 'privacy_contact_email', MAIL_FROM)),
    ];
}

function afLegalDocumentDefinitions(PDO $pdo): array
{
    $controller = afPrivacyController($pdo);
    $name = esc($controller['name']);
    $contact = esc($controller['contact_email']);
    $dpo = esc($controller['dpo_email']);
    $address = esc($controller['address']);
    $document = esc($controller['document']);
    $identity = $name;
    if ($document !== '') $identity .= ' — documento: '.$document;
    if ($address !== '') $identity .= ' — endereço: '.$address;

    return [
        'terms' => [
            'version' => TERMS_VERSION,
            'title' => 'Termos de Uso',
            'summary' => 'Regras para criação de conta, uso do jogo, segurança, conteúdo, pontuação e encerramento da conta.',
            'required' => true,
            'audience' => 'all',
            'body' => '<h2>1. Quem opera o Aura Farming</h2><p>O serviço é disponibilizado por <strong>'.$identity.'</strong>. O canal para dúvidas sobre estes Termos é <strong>'.$contact.'</strong>.</p>'
                .'<h2>2. Aceitação e elegibilidade</h2><p>Ao criar ou continuar usando uma conta, você declara que leu estes Termos. Contas de crianças e adolescentes ficam sujeitas às proteções de idade, vínculo com responsável, consentimentos específicos e controles parentais disponíveis no sistema.</p>'
                .'<h2>3. Conta e segurança</h2><p>Você deve fornecer informações verdadeiras, proteger sua senha e não compartilhar a conta. Podemos limitar sessões, bloquear tentativas abusivas, suspender contas e revisar atividades quando houver indícios de fraude, risco à comunidade ou violação destes Termos.</p>'
                .'<h2>4. Funcionamento do jogo</h2><p>O Aura Farming calcula pontuação a partir de sinais de movimento e regras técnicas da partida. Pontuações podem ser ajustadas, anuladas ou removidas do ranking quando houver falha técnica, manipulação, automação, uso indevido ou revisão de segurança.</p>'
                .'<h2>5. Câmera e recursos protegidos</h2><p>Quando a câmera é utilizada, o aplicativo pode processar sinais de movimento no dispositivo para executar o jogo e aplicar proteções. Recursos para menores dependem das permissões e políticas específicas exibidas ao responsável. O sistema não autoriza transmissão pública de menores.</p>'
                .'<h2>6. Condutas proibidas</h2><p>É proibido fraudar partidas, interferir no serviço, tentar acessar contas de terceiros, publicar conteúdo ilegal, assediar pessoas, explorar menores, burlar controles de idade ou usar o serviço para finalidades que coloquem alguém em risco.</p>'
                .'<h2>7. Conteúdo e interações</h2><p>Você permanece responsável pelo conteúdo que envia. Denúncias podem ser analisadas e conteúdos podem ser removidos quando violarem regras da plataforma, direitos de terceiros ou a legislação aplicável.</p>'
                .'<h2>8. Disponibilidade e mudanças</h2><p>O serviço pode passar por manutenção, alterações e indisponibilidades. Mudanças relevantes nestes Termos serão versionadas e poderão exigir novo aceite.</p>'
                .'<h2>9. Encerramento</h2><p>Você pode solicitar o encerramento da conta pela Central de Privacidade. Alguns registros poderão ser conservados quando houver obrigação legal, necessidade de segurança, prevenção a fraude, exercício de direitos ou outra hipótese permitida.</p>'
                .'<h2>10. Contato</h2><p>Dúvidas sobre o uso do serviço: <strong>'.$contact.'</strong>.</p>',
        ],
        'privacy' => [
            'version' => PRIVACY_POLICY_VERSION,
            'title' => 'Aviso de Privacidade',
            'summary' => 'Explica quais dados são usados, por quê, com quem podem ser compartilhados, por quanto tempo e como exercer seus direitos.',
            'required' => true,
            'audience' => 'all',
            'body' => '<h2>1. Controlador e contato</h2><p>O controlador indicado para o Aura Farming é <strong>'.$identity.'</strong>. Canal de privacidade: <strong>'.$contact.'</strong>. Canal do encarregado ou responsável pelo atendimento: <strong>'.$dpo.'</strong>.</p>'
                .'<h2>2. Dados tratados</h2><p>Podemos tratar dados de cadastro e perfil, data de nascimento e classificação etária, credenciais protegidas, informações de dispositivo e sessão, endereço IP, registros de segurança, pontuações, partidas, interações, denúncias, notificações, vínculos com responsáveis, consentimentos, protocolos de revisão e solicitações de privacidade. Na conferência do responsável, o número do documento é usado apenas em memória para gerar uma impressão HMAC não reversível e não é armazenado em formato bruto.</p>'
                .'<h2>3. Câmera e movimento</h2><p>Nas partidas com câmera, sinais de movimento podem ser processados para pontuação, integridade da partida e segurança. No modo protegido para menores, o projeto foi estruturado para processar a detecção no aparelho e registrar somente metadados técnicos de sessão e incidentes, sem armazenar vídeo bruto, fotografias, áudio ou pontos corporais no painel do responsável.</p>'
                .'<h2>4. Finalidades</h2><p>Os dados são usados para criar e autenticar contas, executar partidas, calcular progressão, prevenir fraude, proteger usuários, aplicar regras etárias, atender responsáveis, moderar denúncias, enviar comunicações operacionais, cumprir obrigações e responder a solicitações do titular.</p>'
                .'<h2>5. Bases e necessidade</h2><p>O tratamento pode ocorrer para executar o serviço solicitado, cumprir obrigações, proteger direitos e segurança, prevenir fraude, atender interesses legítimos avaliados e, quando aplicável, com consentimento específico. A base usada depende da operação concreta.</p>'
                .'<h2>6. Compartilhamento</h2><p>Dados podem ser compartilhados com fornecedores estritamente necessários de hospedagem, e-mail, autenticação, segurança e suporte, além de autoridades quando houver obrigação ou solicitação válida. O sistema deve limitar o compartilhamento ao necessário.</p>'
                .'<h2>7. Retenção</h2><p>Os dados são mantidos pelo período necessário às finalidades informadas e às exigências legais, de segurança e defesa de direitos. Solicitações de eliminação são avaliadas considerando as hipóteses legais de conservação.</p>'
                .'<h2>8. Segurança</h2><p>São adotados controles de sessão, senhas protegidas, limitação de tentativas, registros de auditoria e políticas de acesso. Nenhum ambiente é totalmente imune a incidentes; eventos relevantes serão tratados conforme o plano de segurança aplicável.</p>'
                .'<h2>9. Seus direitos</h2><p>A Central de Privacidade permite pedir confirmação e acesso, correção, anonimização, bloqueio ou eliminação, portabilidade, informação sobre compartilhamentos, revogação de consentimento, oposição e revisão de decisões automatizadas, quando aplicável.</p>'
                .'<h2>10. Atualizações</h2><p>Este aviso possui versão própria. Alterações relevantes podem exigir novo aceite e ficarão registradas no histórico da conta.</p>',
        ],
        'child_privacy' => [
            'version' => CHILD_PRIVACY_VERSION,
            'title' => 'Aviso de Privacidade para Crianças, Adolescentes e Responsáveis',
            'summary' => 'Apresenta as proteções adicionais aplicadas a contas de menores e os controles disponíveis ao responsável.',
            'required' => true,
            'audience' => 'minor_guardian',
            'body' => '<h2>1. Melhor interesse e proteção reforçada</h2><p>Contas de crianças e adolescentes recebem restrições adicionais. O sistema considera idade declarada, vínculo com responsável, verificação do adulto, consentimentos específicos, horário permitido, limite diário e disponibilidade da câmera segura.</p>'
                .'<h2>2. O que o menor pode ver e fazer</h2><p>O acesso depende da faixa etária e das autorizações vigentes. Recursos públicos de alto risco, como transmissão pública, mensagens de desconhecidos, localização pública, replay com imagem real e download de vídeo, permanecem bloqueados para menores.</p>'
                .'<h2>3. Papel do responsável</h2><p>O responsável passa por verificação interna em camadas, incluindo declaração, e-mail, código familiar e conferência humana presencial ou ao vivo sem gravação. O documento original e a prova de responsabilidade podem ser apenas visualizados; nenhuma cópia é anexada. O responsável administra consentimentos, pausa recursos, define horários e limites, acompanha metadados de sessões e recebe alertas técnicos. O painel não exibe vídeo bruto, imagens, áudio ou pontos corporais.</p>'
                .'<h2>4. Dados usados para proteção</h2><p>Podem ser tratados idade declarada, vínculo familiar informado, histórico de consentimentos, controles parentais, sessões protegidas, incidentes técnicos e decisões do motor de políticas. Esses registros ajudam a aplicar as restrições e demonstrar as decisões de segurança.</p>'
                .'<h2>5. Direitos</h2><p>O menor e seu representante podem usar a Central de Privacidade para solicitar acesso, correção, explicações, restrição ou eliminação, observadas as regras aplicáveis e as hipóteses de conservação.</p>'
                .'<h2>6. Contato</h2><p>Canal de privacidade: <strong>'.$contact.'</strong>.</p>',
        ],
        'guardian_terms' => [
            'version' => GUARDIAN_TERMS_VERSION,
            'title' => 'Termo do Responsável Legal',
            'summary' => 'Responsabilidades do adulto verificado, supervisão contínua, contatos aprovados e revogação de permissões.',
            'required' => true,
            'audience' => 'guardian',
            'body' => '<h2>1. Verificação interna do responsável</h2><p>O responsável declara ser maior de 18 anos e possuir responsabilidade legal, confirma o e-mail da conta, informa um código familiar gerado dentro da conta autenticada do menor e passa por conferência presencial ou ao vivo sem gravação. O documento original e a prova de vínculo podem ser apenas visualizados. O sistema guarda protocolo, checklist e uma impressão HMAC não reversível do número do documento, sem guardar o número, fotografia, áudio, vídeo ou cópia. Não existe aprovação automática por uma única caixa.</p>'
                .'<h2>2. Supervisão contínua</h2><p>O responsável deve revisar permissões, horários, contatos aprovados, incidentes e alertas. As autorizações podem ser revogadas a qualquer momento no painel familiar.</p>'
                .'<h2>3. Recursos de risco</h2><p>Transmissão pública, mensagens de desconhecidos, localização pública, replay com imagem real e download de vídeo permanecem bloqueados para menores. Battle depende de contato aprovado.</p>'
                .'<h2>4. Ambiente da câmera</h2><p>O responsável deve orientar o uso em ambiente privado, sem filmar terceiros. A pontuação pode ser pausada quando outra pessoa, iluminação inadequada ou falha de privacidade forem detectadas.</p>'
                .'<h2>5. Privacidade e direitos</h2><p>O responsável pode solicitar cópia, correção, exclusão de histórico ou encerramento da conta pela Central de Privacidade. Canal: <strong>'.$contact.'</strong>.</p>',
        ],
        'camera_transmission' => [
            'version' => CAMERA_POLICY_VERSION,
            'title' => 'Política de Câmera e Transmissão',
            'summary' => 'Explica processamento local, detecção de terceiros, iluminação, desfoque, gravação e transmissão.',
            'required' => true,
            'audience' => 'all',
            'body' => '<h2>1. Finalidade da câmera</h2><p>A câmera é usada para detectar movimentos, calcular pontuação e aplicar proteções de segurança. O usuário deve escolher ambiente privado e evitar enquadrar terceiros.</p>'
                .'<h2>2. Processamento e minimização</h2><p>Para contas protegidas, corpo, movimento, quantidade de pessoas e iluminação são processados no aparelho sempre que possível. O servidor recebe somente metadados técnicos necessários, sem vídeo bruto, fotografia, áudio ou landmarks corporais.</p>'
                .'<h2>3. Terceiros no enquadramento</h2><p>Quando outra pessoa aparece, a pontuação pode ser pausada, o fundo permanece desfocado e rostos adicionais recebem proteção visual. O evento técnico pode ser registrado e comunicado ao responsável sem armazenar a imagem.</p>'
                .'<h2>4. Gravação e transmissão</h2><p>Gravação fica desativada por padrão. Transmissão somente ocorre quando a permissão correspondente está ativa. Transmissão pública de menores permanece proibida nesta fase.</p>'
                .'<h2>5. Iluminação e falhas</h2><p>A partida pode ser impedida ou pausada quando a iluminação for insuficiente ou excessiva, quando o detector não estiver disponível ou quando a proteção visual falhar.</p>',
        ],
        'retention_deletion' => [
            'version' => RETENTION_POLICY_VERSION,
            'title' => 'Política de Retenção e Exclusão',
            'summary' => 'Prazos, solicitações de exclusão, preservação necessária e eliminação segura.',
            'required' => false,
            'audience' => 'all',
            'body' => '<h2>1. Regra geral</h2><p>Os dados são conservados somente enquanto necessários às finalidades informadas, à segurança, à prevenção a fraude, ao cumprimento de obrigações e à defesa de direitos.</p>'
                .'<h2>2. Solicitações</h2><p>A Central de Privacidade permite solicitar exclusão integral da conta ou somente do histórico. Cada solicitação recebe protocolo, status e registro de atendimento.</p>'
                .'<h2>3. Exceções de conservação</h2><p>Alguns registros podem ser bloqueados ou preservados pelo período necessário quando houver obrigação legal, incidente, denúncia, prevenção a fraude ou exercício regular de direitos. Protocolos, checklists e impressões HMAC de segurança podem ser mantidos enquanto necessários para demonstrar a revisão e impedir reutilização indevida, sem conservar o documento original ou seu número bruto.</p>'
                .'<h2>4. Menores e eventos</h2><p>Dados de menores recebem prioridade de minimização. Eventos infantis devem possuir prazo de exclusão automática definido pelo organizador e pelo responsável, observadas as exceções legais.</p>'
                .'<h2>5. Exclusão segura</h2><p>Após a aprovação, os dados são eliminados, anonimizados ou isolados conforme o escopo e a possibilidade técnica e legal, mantendo a trilha mínima de atendimento.</p>',
        ],
        'community_rules' => [
            'version' => COMMUNITY_RULES_VERSION,
            'title' => 'Regras da Comunidade',
            'summary' => 'Condutas permitidas, proteção de menores, denúncias, bloqueios e consequências.',
            'required' => true,
            'audience' => 'all',
            'body' => '<h2>1. Respeito e segurança</h2><p>Não são permitidos assédio, intimidação, discriminação, exploração, conteúdo sexual envolvendo menores, ameaças, fraude ou incentivo a comportamento perigoso.</p>'
                .'<h2>2. Informações pessoais</h2><p>É proibido pedir ou divulgar telefone, endereço, localização, escola, documentos, senhas, redes sociais ou outros dados de contato de crianças e adolescentes.</p>'
                .'<h2>3. Links e contatos externos</h2><p>Não envie links suspeitos nem tente retirar menores do ambiente protegido da plataforma. Contatos de menores dependem de aprovação do responsável.</p>'
                .'<h2>4. Denúncias e medidas</h2><p>Usuários podem denunciar e bloquear contas. A plataforma pode ocultar conteúdo, interromper interação, suspender preventivamente, aplicar sanções e comunicar o responsável.</p>'
                .'<h2>5. Reincidência</h2><p>Violações repetidas ou graves podem resultar em suspensão prolongada ou encerramento da conta, preservando registros necessários à análise e à segurança.</p>',
        ],
        'cookies' => [
            'version' => COOKIE_NOTICE_VERSION,
            'title' => 'Aviso de Cookies e Armazenamento Local',
            'summary' => 'Explica os cookies técnicos usados para login, segurança e preferências.',
            'required' => false,
            'audience' => 'all',
            'body' => '<h2>1. Cookies necessários</h2><p>O Aura Farming usa cookies e identificadores técnicos necessários para manter a sessão autenticada, proteger formulários, reconhecer o dispositivo e preservar a segurança da conta.</p>'
                .'<h2>2. Preferências locais</h2><p>O navegador pode guardar preferências de interface e informações operacionais. Esses registros não devem ser usados para publicidade comportamental sem uma configuração e um aviso específicos.</p>'
                .'<h2>3. Controle</h2><p>Bloquear cookies essenciais pode impedir login, partidas e recursos de segurança. Você pode gerenciar cookies nas configurações do navegador.</p>',
        ],
    ];
}

function afSyncLegalDocuments(PDO $pdo): void
{
    static $done = false;
    if ($done) return;
    $done = true;
    if (!afPrivacyTableExists($pdo, 'af_legal_documents')) return;
    foreach (afLegalDocumentDefinitions($pdo) as $key => $doc) {
        $hash = hash('sha256', $key.'|'.$doc['version'].'|'.$doc['title'].'|'.$doc['summary'].'|'.$doc['body']);
        $check = $pdo->prepare('SELECT id,content_hash,active FROM af_legal_documents WHERE document_key=? AND version_key=? LIMIT 1');
        $check->execute([$key,$doc['version']]);
        $existing = $check->fetch();
        if (!$existing || !hash_equals((string)$existing['content_hash'],$hash) || !(int)$existing['active']) {
            $st = $pdo->prepare("INSERT INTO af_legal_documents
                (document_key,version_key,title,summary,content_hash,content_html,required_acceptance,audience,active,effective_at,created_at,updated_at)
                VALUES (?,?,?,?,?,?,?,?,1,NOW(),NOW(),NOW())
                ON DUPLICATE KEY UPDATE title=VALUES(title),summary=VALUES(summary),content_hash=VALUES(content_hash),content_html=VALUES(content_html),required_acceptance=VALUES(required_acceptance),audience=VALUES(audience),active=1,updated_at=NOW()");
            $st->execute([$key,$doc['version'],$doc['title'],$doc['summary'],$hash,$doc['body'],$doc['required']?1:0,$doc['audience']]);
        }
        $old = $pdo->prepare('UPDATE af_legal_documents SET active=0,updated_at=NOW() WHERE document_key=? AND version_key<>? AND active=1');
        $old->execute([$key,$doc['version']]);
    }
}

function afLegalDocument(PDO $pdo, string $key): ?array
{
    afSyncLegalDocuments($pdo);
    if (!afPrivacyTableExists($pdo, 'af_legal_documents')) return null;
    $st = $pdo->prepare('SELECT * FROM af_legal_documents WHERE document_key=? AND active=1 ORDER BY id DESC LIMIT 1');
    $st->execute([$key]);
    return $st->fetch() ?: null;
}

function afRequiredLegalDocuments(PDO $pdo, int $userId): array
{
    afSyncLegalDocuments($pdo);
    if (!afPrivacyTableExists($pdo, 'af_legal_documents')) return [];
    $age = function_exists('afResumoIdade') ? afResumoIdade($pdo, $userId) : [];
    $type=(string)($age['tipo_conta']??'');
    $audiences=['all'];
    if($type==='minor')$audiences[]='minor_guardian';
    if($type==='guardian'){$audiences[]='minor_guardian';$audiences[]='guardian';}
    $quoted=implode(',',array_map(static fn(string $v):string=>$pdo->quote($v),array_unique($audiences)));
    return $pdo->query("SELECT * FROM af_legal_documents WHERE active=1 AND required_acceptance=1 AND audience IN ({$quoted}) ORDER BY id")->fetchAll() ?: [];
}

function afMissingLegalAcceptances(PDO $pdo, int $userId): array
{
    if ($userId <= 0 || !afPrivacyTableExists($pdo, 'af_legal_acceptances')) return [];
    $missing = [];
    $st = $pdo->prepare('SELECT id FROM af_legal_acceptances WHERE user_id=? AND legal_document_id=? LIMIT 1');
    foreach (afRequiredLegalDocuments($pdo, $userId) as $doc) {
        $st->execute([$userId,(int)$doc['id']]);
        if (!$st->fetchColumn()) $missing[] = $doc;
    }
    return $missing;
}

function afRecordLegalAcceptances(PDO $pdo, int $userId, array $documentIds, string $source = 'web', ?int $actorUserId = null): int
{
    if (!afPrivacyTableExists($pdo, 'af_legal_acceptances')) throw new RuntimeException('A migração da Etapa 21 ainda não foi importada.');
    $required = afRequiredLegalDocuments($pdo, $userId);
    $allowed = [];
    foreach ($required as $doc) $allowed[(int)$doc['id']] = $doc;
    foreach ($documentIds as $id) {
        $id = (int)$id;
        if (!isset($allowed[$id])) throw new RuntimeException('Documento legal inválido ou desatualizado.');
    }
    foreach (array_keys($allowed) as $id) {
        if (!in_array($id, array_map('intval',$documentIds), true)) throw new RuntimeException('Aceite todos os documentos obrigatórios para continuar.');
    }
    $actorUserId = $actorUserId ?: $userId;
    $count = 0;
    $st = $pdo->prepare("INSERT IGNORE INTO af_legal_acceptances
        (user_id,actor_user_id,legal_document_id,source,ip_address,user_agent,device_uuid,accepted_at)
        VALUES (?,?,?,?,?,?,?,NOW())");
    foreach (array_keys($allowed) as $id) {
        $st->execute([$userId,$actorUserId,$id,substr($source,0,40),clientIp(),substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,1000),substr(deviceUuid(),0,64)]);
        $inserted=$st->rowCount();$count += $inserted;
        if($inserted && afPrivacyTableExists($pdo,'af_legal_acceptance_audit_logs')){
            $pdo->prepare('INSERT INTO af_legal_acceptance_audit_logs (user_id,actor_user_id,legal_document_id,action_key,source,ip_address,user_agent,device_uuid,created_at) VALUES (?,?,?,?,?,?,?,?,NOW())')
                ->execute([$userId,$actorUserId,$id,'accepted',substr($source,0,40),clientIp(),substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,1000),substr(deviceUuid(),0,64)]);
        }
    }
    return $count;
}

function afRecordRegistrationLegalAcceptances(PDO $pdo, int $userId, string $source = 'registration'): void
{
    $ids = array_map(static fn(array $d): int => (int)$d['id'], afRequiredLegalDocuments($pdo, $userId));
    if ($ids) afRecordLegalAcceptances($pdo,$userId,$ids,$source,$userId);
}

function afLegalAllowedWebPath(string $script): bool
{
    foreach ([
        '/pages/aceitar-termos.php','/pages/termos-de-uso.php','/pages/privacidade.php',
        '/pages/privacidade-menores.php','/pages/termo-responsavel.php','/pages/politica-camera-transmissao.php',
        '/pages/politica-retencao-exclusao.php','/pages/regras-comunidade.php','/pages/cookies.php','/pages/central-privacidade.php',
        '/pages/exportar-dados.php','/auth/logout.php'
    ] as $path) if (str_ends_with($script,$path)) return true;
    return false;
}

function afEnforceLegalAcceptance(PDO $pdo, int $userId): void
{
    if ($userId <= 0 || !afPrivacyTableExists($pdo,'af_legal_documents')) return;
    $script = str_replace('\\','/',(string)($_SERVER['SCRIPT_NAME']??''));
    if (afLegalAllowedWebPath($script)) return;
    $missing = afMissingLegalAcceptances($pdo,$userId);
    if (!$missing) return;
    $uri = (string)($_SERVER['REQUEST_URI']??'/pages/home.php');
    if (str_starts_with($uri,'/') && !str_starts_with($uri,'//')) $_SESSION['af_after_legal']=$uri;
    setFlash('error','Leia e aceite os documentos atualizados para continuar.');
    header('Location: '.SITE_URL.'/pages/aceitar-termos.php');
    exit;
}

function afAssertApiLegalAccess(PDO $pdo, array $user): void
{
    if (!afPrivacyTableExists($pdo,'af_legal_documents')) return;
    $path = str_replace('\\','/',(string)($_SERVER['SCRIPT_NAME']??''));
    if (str_contains($path,'/api/v1/privacy/')) return;
    $missing = afMissingLegalAcceptances($pdo,(int)($user['id']??0));
    if (!$missing) return;
    jsonResponse([
        'ok'=>false,
        'code'=>'LEGAL_ACCEPTANCE_REQUIRED',
        'erro'=>'Aceite os Termos e o Aviso de Privacidade atualizados.',
        'documents'=>array_map(static fn(array $d): array => ['id'=>(int)$d['id'],'key'=>$d['document_key'],'version'=>$d['version_key'],'title'=>$d['title']],$missing),
    ],403);
}

function afPrivacyRequestTypes(): array
{
    return [
        'confirmation_access'=>'Confirmação e acesso aos dados',
        'correction'=>'Correção de dados',
        'anonymization_blocking_deletion'=>'Anonimização, bloqueio ou eliminação',
        'portability'=>'Portabilidade',
        'sharing_information'=>'Informações sobre compartilhamento',
        'consent_revocation'=>'Revogação de consentimento',
        'opposition'=>'Oposição ao tratamento',
        'automated_decision_review'=>'Revisão de decisão automatizada',
        'account_deletion'=>'Encerramento da conta',
        'history_deletion'=>'Exclusão do histórico de partidas e atividades',
        'other'=>'Outro assunto de privacidade',
    ];
}

function afPrivacyRequestStatuses(): array
{
    return [
        'open'=>'Recebida',
        'in_review'=>'Em análise',
        'awaiting_identity'=>'Aguardando confirmação',
        'completed'=>'Concluída',
        'denied'=>'Não atendida',
        'cancelled'=>'Cancelada',
    ];
}

function afPrivacyCanActForSubject(PDO $pdo, int $requesterId, int $subjectId): array
{
    if ($requesterId <= 0 || $subjectId <= 0) return ['allowed'=>false,'link_id'=>null,'reason'=>'Conta inválida.'];
    if ($requesterId === $subjectId) return ['allowed'=>true,'link_id'=>null,'reason'=>'self'];
    if (!afPrivacyTableExists($pdo,'af_guardian_minor_links')) return ['allowed'=>false,'link_id'=>null,'reason'=>'Vínculo não encontrado.'];
    $st=$pdo->prepare("SELECT id,status FROM af_guardian_minor_links WHERE guardian_usuario_id=? AND minor_usuario_id=? AND family_confirmed_at IS NOT NULL AND status IN ('active','verified_pending_consent') LIMIT 1");
    $st->execute([$requesterId,$subjectId]);
    $link=$st->fetch();
    if(!$link) return ['allowed'=>false,'link_id'=>null,'reason'=>'Você não possui vínculo ativo com essa conta.'];
    if(function_exists('afGuardianVerificationStatus')){
        $verification=afGuardianVerificationStatus($pdo,$requesterId);
        if(empty($verification['approved'])) return ['allowed'=>false,'link_id'=>(int)$link['id'],'reason'=>'Conclua a verificação do responsável.'];
    }
    return ['allowed'=>true,'link_id'=>(int)$link['id'],'reason'=>'guardian'];
}

function afPrivacySubjects(PDO $pdo, int $requesterId): array
{
    $st=$pdo->prepare('SELECT id,nome,username,aura_id FROM af_usuarios WHERE id=? LIMIT 1');
    $st->execute([$requesterId]);
    $items=[];
    if($u=$st->fetch()) $items[]=['id'=>(int)$u['id'],'label'=>$u['nome'].' (@'.$u['username'].')','relation'=>'Minha conta'];
    $guardianVerified=true;
    if(function_exists('afGuardianVerificationStatus')){
        $verification=afGuardianVerificationStatus($pdo,$requesterId);
        $guardianVerified=!empty($verification['approved']);
    }
    if($guardianVerified&&afPrivacyTableExists($pdo,'af_guardian_minor_links')){
        $st=$pdo->prepare("SELECT u.id,u.nome,u.username,u.aura_id,l.parentesco FROM af_guardian_minor_links l JOIN af_usuarios u ON u.id=l.minor_usuario_id WHERE l.guardian_usuario_id=? AND l.family_confirmed_at IS NOT NULL AND l.status IN ('active','verified_pending_consent') ORDER BY u.nome");
        $st->execute([$requesterId]);
        foreach($st->fetchAll()?:[] as $u) $items[]=['id'=>(int)$u['id'],'label'=>$u['nome'].' (@'.$u['username'].')','relation'=>'Dependente • '.$u['parentesco']];
    }
    return $items;
}

function afPrivacyProtocol(): string
{
    return 'AF-PRIV-'.date('Ym').'-'.strtoupper(bin2hex(random_bytes(4)));
}

function afCreatePrivacyRequest(PDO $pdo, int $requesterId, int $subjectId, string $type, string $description): array
{
    if(!afPrivacyTableExists($pdo,'af_privacy_requests')) throw new RuntimeException('A migração da Etapa 21 ainda não foi importada.');
    $types=afPrivacyRequestTypes();
    if(!isset($types[$type])) throw new RuntimeException('Tipo de solicitação inválido.');
    $auth=afPrivacyCanActForSubject($pdo,$requesterId,$subjectId);
    if(!$auth['allowed']) throw new RuntimeException((string)$auth['reason']);
    $description=trim($description);
    if(mb_strlen($description)<10) throw new RuntimeException('Descreva sua solicitação com pelo menos 10 caracteres.');
    if(mb_strlen($description)>3000) throw new RuntimeException('A descrição deve ter no máximo 3.000 caracteres.');
    $protocol=afPrivacyProtocol();
    $targetDays=max(1,min(90,(int)afPrivacyConfig($pdo,'privacy_request_target_days','15')));
    $targetAt=(new DateTimeImmutable('+'.$targetDays.' days'))->format('Y-m-d H:i:s');
    $st=$pdo->prepare("INSERT INTO af_privacy_requests
        (protocol,requester_user_id,subject_user_id,guardian_link_id,request_type,status,description,identity_status,submitted_at,target_response_at,ip_address,user_agent,device_uuid,updated_at)
        VALUES (?,?,?,?,?,'open',?,'authenticated',NOW(),?,?,?,?,NOW())");
    $st->execute([$protocol,$requesterId,$subjectId,$auth['link_id'],$type,$description,$targetAt,clientIp(),substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,1000),substr(deviceUuid(),0,64)]);
    $id=(int)$pdo->lastInsertId();
    if(afPrivacyTableExists($pdo,'af_privacy_request_events')){
        $pdo->prepare("INSERT INTO af_privacy_request_events (privacy_request_id,actor_type,actor_user_id,event_key,old_status,new_status,details_json,ip_address,created_at) VALUES (?,'user',?,'created',NULL,'open',?,?,NOW())")
            ->execute([$id,$requesterId,json_encode(['request_type'=>$type],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),clientIp()]);
    }
    if(in_array($type,['account_deletion','history_deletion'],true) && function_exists('afPrivacyCreateDeletionRecord'))afPrivacyCreateDeletionRecord($pdo,$id,$requesterId,$subjectId,$type==='history_deletion'?'history':'account');
    criarNotificacao($pdo,$requesterId,'privacy_request','Solicitação de privacidade recebida','Protocolo '.$protocol.'. Acompanhe o andamento na Central de Privacidade.',['privacy_request_id'=>$id,'protocol'=>$protocol]);
    return ['id'=>$id,'protocol'=>$protocol];
}

function afPrivacyRequestsForUser(PDO $pdo, int $requesterId): array
{
    if(!afPrivacyTableExists($pdo,'af_privacy_requests')) return [];
    $st=$pdo->prepare("SELECT r.*,s.nome subject_name,s.username subject_username FROM af_privacy_requests r JOIN af_usuarios s ON s.id=r.subject_user_id WHERE r.requester_user_id=? ORDER BY r.id DESC LIMIT 100");
    $st->execute([$requesterId]);
    return $st->fetchAll()?:[];
}

function afCancelPrivacyRequest(PDO $pdo, int $requesterId, int $requestId): bool
{
    $st=$pdo->prepare("UPDATE af_privacy_requests SET status='cancelled',cancelled_at=NOW(),updated_at=NOW() WHERE id=? AND requester_user_id=? AND status IN ('open','awaiting_identity')");
    $st->execute([$requestId,$requesterId]);$ok=$st->rowCount()>0;
    if($ok && afPrivacyTableExists($pdo,'af_privacy_request_events'))$pdo->prepare("INSERT INTO af_privacy_request_events (privacy_request_id,actor_type,actor_user_id,event_key,old_status,new_status,ip_address,created_at) VALUES (?,'user',?,'cancelled','open','cancelled',?,NOW())")->execute([$requestId,$requesterId,clientIp()]);
    if($ok && afPrivacyTableExists($pdo,'af_deletion_requests'))$pdo->prepare("UPDATE af_deletion_requests SET status='cancelled',updated_at=NOW() WHERE privacy_request_id=?")->execute([$requestId]);
    return $ok;
}

function afPrivacySafeColumns(PDO $pdo, string $table, array $wanted): array
{
    if(!afPrivacyTableExists($pdo,$table)) return [];
    try{
        $columns=$pdo->query('SHOW COLUMNS FROM `'.$table.'`')->fetchAll(PDO::FETCH_COLUMN)?:[];
        return array_values(array_intersect($wanted,$columns));
    }catch(Throwable){return [];}
}

function afPrivacySelectRows(PDO $pdo, string $table, array $wanted, string $where, array $params, string $order=''): array
{
    $columns=afPrivacySafeColumns($pdo,$table,$wanted);
    if(!$columns) return [];
    try{
        $sql='SELECT '.implode(',',array_map(static fn(string $c):string=>'`'.$c.'`',$columns)).' FROM `'.$table.'` WHERE '.$where.($order?' ORDER BY '.$order:'');
        $st=$pdo->prepare($sql);$st->execute($params);return $st->fetchAll()?:[];
    }catch(Throwable){return [];}
}

function afPrivacyBuildExport(PDO $pdo, int $requesterId, int $subjectId): array
{
    $auth=afPrivacyCanActForSubject($pdo,$requesterId,$subjectId);
    if(!$auth['allowed']) throw new RuntimeException((string)$auth['reason']);
    $controller=afPrivacyController($pdo);
    $account=afPrivacySelectRows($pdo,'af_usuarios',['id','nome','username','email','foto_perfil','foto_externa','aura_id','auth_provider','email_verificado_em','nivel','xp_total','aura_total','melhor_pontuacao','total_partidas','total_vitorias','total_likes','total_superlikes','titulo','status','data_cadastro','ultimo_login','atualizado_em'],'id=?',[$subjectId]);
    $data=[
        'export_metadata'=>[
            'generated_at'=>date(DATE_ATOM),
            'controller'=>$controller['name'],
            'requester_user_id'=>$requesterId,
            'subject_user_id'=>$subjectId,
            'format_version'=>'1.0',
            'notice'=>'O arquivo não contém senha, tokens de sessão, número ou impressão HMAC de documento, segredos internos de segurança, vídeo bruto, imagens da câmera, áudio ou landmarks corporais.',
        ],
        'account'=>$account[0]??null,
        'profile'=>(afPrivacySelectRows($pdo,'af_perfis',['bio','cidade','estado','pais','perfil_privado','criado_em','atualizado_em'],'usuario_id=?',[$subjectId]))[0]??null,
        'age_profile'=>(afPrivacySelectRows($pdo,'af_user_age_profiles',['data_nascimento','pais_residencia','tipo_conta','status_idade','idade_declarada','origem_declaracao','declarado_em','verificado_em','atualizado_em'],'usuario_id=?',[$subjectId]))[0]??null,
        'guardian_account'=>(afPrivacySelectRows($pdo,'af_guardian_accounts',['id','telefone','parentesco_padrao','status_verificacao','metodo_verificacao','email_verificado_em','declaracao_aceita_em','data_nascimento_confirmada','verificado_em','criado_em','atualizado_em'],'usuario_id=?',[$subjectId]))[0]??null,
        'guardian_verification'=>(afPrivacySelectRows($pdo,'af_guardian_verifications',['id','guardian_account_id','metodo','status','data_nascimento_confirmada','data_nascimento_confirmada_em','declaration_version','declaration_text_hash','declaracao_aceita_em','email_verificado_em','resultado','score_risco','motivo_rejeicao','adulto_verificado_em','criado_em','atualizado_em'],'guardian_account_id IN (SELECT id FROM af_guardian_accounts WHERE usuario_id=?)',[$subjectId],'id DESC'))[0]??null,
        'guardian_manual_reviews'=>afPrivacySelectRows($pdo,'af_guardian_manual_reviews',['id','guardian_account_id','verification_id','status','risk_score','risk_flags_json','submitted_at','review_method','reviewer_checklist_json','review_protocol','reviewer_notes','reviewed_at','valid_until','created_at','updated_at'],'guardian_account_id IN (SELECT id FROM af_guardian_accounts WHERE usuario_id=?)',[$subjectId],'id DESC'),
        'oauth_accounts'=>afPrivacySelectRows($pdo,'af_oauth_contas',['provider','provider_user_id','email','criada_em'],'usuario_id=?',[$subjectId],'id'),
        'devices'=>afPrivacySelectRows($pdo,'af_dispositivos',['id','device_uuid','nome','plataforma','navegador','user_agent','ip_address','confiavel','ativo','criado_em','ultimo_acesso'],'usuario_id=?',[$subjectId],'id DESC'),
        'sessions'=>afPrivacySelectRows($pdo,'af_sessoes',['id','dispositivo_id','ip_address','user_agent','criada_em','ultima_atividade','expira_em','revogada_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'matches'=>afPrivacySelectRows($pdo,'af_partidas',['id','sessao_id','aura_gerada','likes_recebidos','superlikes_recebidos','bonus_combo','pontuacao_total','pontuacao_validada','status','antifraude_status','ip_address','iniciada_em','finalizada_em','criada_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'notifications'=>afPrivacySelectRows($pdo,'af_notificacoes',['id','tipo','titulo','mensagem','dados','lida_em','criada_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'social_follows'=>afPrivacySelectRows($pdo,'af_seguidores',['id','seguidor_id','seguido_id','criado_em'],'seguidor_id=? OR seguido_id=?',[$subjectId,$subjectId],'id DESC'),
        'match_interactions'=>afPrivacySelectRows($pdo,'af_partida_interacoes',['id','partida_id','tipo','valor','aceita','risco','motivo_rejeicao','criada_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'shares'=>afPrivacySelectRows($pdo,'af_compartilhamentos',['id','partida_id','canal','criado_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'competition_participation'=>afPrivacySelectRows($pdo,'af_competicao_participantes',['id','competicao_id','seed','status','vitorias','derrotas','empates','partidas','pontuacao_total','melhor_pontuacao','posicao','entrou_em','atualizado_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'battle_participation'=>afPrivacySelectRows($pdo,'af_batalha_jogadores',['id','batalha_id','slot','status','partida_id','pontuacao','aura','pontuacao_social','combo_maximo','aceito_em','camera_pronta_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'legacy_reports'=>afPrivacySelectRows($pdo,'af_denuncias',['id','denunciante_id','denunciado_id','partida_id','competicao_id','categoria','descricao','status','prioridade','resolucao','criado_em','atualizado_em','resolvido_em'],'denunciante_id=? OR denunciado_id=?',[$subjectId,$subjectId],'id DESC'),
        'safety_reports'=>afPrivacySelectRows($pdo,'af_safety_reports',['id','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','resolution','guardian_notified_at','resolved_at','created_at','updated_at'],'reporter_user_id=? OR reported_user_id=?',[$subjectId,$subjectId],'id DESC'),
        'user_blocks'=>afPrivacySelectRows($pdo,'af_user_blocks',['id','blocker_user_id','blocked_user_id','source','report_id','reason','active','created_at','revoked_at','updated_at'],'blocker_user_id=? OR blocked_user_id=?',[$subjectId,$subjectId],'id DESC'),
        'moderation_actions'=>afPrivacySelectRows($pdo,'af_moderation_actions',['id','report_id','target_user_id','target_partida_id','target_comment_id','action_type','reason','starts_at','ends_at','metadata_json','revoked_at','created_at'],'target_user_id=?',[$subjectId],'id DESC'),
        'minor_safety_incidents'=>afPrivacySelectRows($pdo,'af_minor_safety_incidents',['id','minor_user_id','related_user_id','report_id','partida_id','incident_type','severity','summary','details_json','status','guardian_notified_at','resolved_at','created_at','updated_at'],'minor_user_id=? OR related_user_id=?',[$subjectId,$subjectId],'id DESC'),
        'live_comments'=>afPrivacySelectRows($pdo,'af_live_comments',['id','partida_id','author_user_id','body_original','body_visible','status','risk_score','filter_hits_json','created_at','updated_at'],'author_user_id=?',[$subjectId],'id DESC'),
        'live_spectator_restrictions'=>afPrivacySelectRows($pdo,'af_live_spectator_restrictions',['id','partida_id','owner_user_id','spectator_user_id','restriction_type','reason','active','created_at','expires_at','revoked_at'],'owner_user_id=? OR spectator_user_id=?',[$subjectId,$subjectId],'id DESC'),
        'age_history'=>afPrivacySelectRows($pdo,'af_age_status_history',['id','status_anterior','status_novo','motivo','dados','criado_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'safe_camera_sessions'=>afPrivacySelectRows($pdo,'af_safe_camera_sessions',['id','partida_id','policy_version','is_minor','privacy_mode','detector_status','state','pause_reason','person_count','blur_enabled','privacy_rendering','tab_visible','camera_active','ready_started_at','pause_started_at','pause_total_seconds','multiple_people_incidents','no_person_incidents','visibility_incidents','camera_incidents','lighting_score','lighting_ok','lighting_incidents','additional_faces_blurred','last_frame_at','last_event_at','criado_em','atualizado_em'],'usuario_id=?',[$subjectId],'id DESC'),
        'guardian_controls'=>afPrivacySelectRows($pdo,'af_guardian_controls',['id','link_id','guardian_usuario_id','minor_usuario_id','access_status','daily_limit_minutes','allowed_start_time','allowed_end_time','timezone','camera_enabled','farm_enabled','battle_enabled','ranking_enabled','transmission_enabled','profile_private','comments_moderated','notify_session_start','notify_session_end','notify_security_incident','notify_weekly_summary','criado_em','atualizado_em'],'guardian_usuario_id=? OR minor_usuario_id=?',[$subjectId,$subjectId],'id DESC'),
        'minor_safety_settings'=>(afPrivacySelectRows($pdo,'af_minor_safety_settings',['profile_private','alias_only','hide_last_name','hide_city','hide_location','block_stranger_messages','block_public_search','block_public_share','block_real_replay','block_image_download','comments_mode','created_at','updated_at'],'minor_usuario_id=?',[$subjectId]))[0]??null,
        'camera_privacy_settings'=>(afPrivacySelectRows($pdo,'af_camera_privacy_settings',['blur_background','blur_additional_faces','recording_enabled','processing_mode','transmission_allowed','created_at','updated_at'],'minor_usuario_id=?',[$subjectId]))[0]??null,
        'feature_permissions'=>afPrivacySelectRows($pdo,'af_feature_permissions',['feature_key','allowed','source','guardian_usuario_id','changed_at','created_at','updated_at'],'minor_usuario_id=?',[$subjectId],'id DESC'),
        'approved_contacts'=>afPrivacySelectRows($pdo,'af_approved_contacts',['contact_usuario_id','guardian_usuario_id','status','notes','approved_at','revoked_at','created_at','updated_at'],'minor_usuario_id=?',[$subjectId],'id DESC'),
        'minor_access_logs'=>afPrivacySelectRows($pdo,'af_minor_access_logs',['feature_key','allowed','reason_code','created_at'],'minor_usuario_id=?',[$subjectId],'id DESC'),
        'third_party_detections'=>afPrivacySelectRows($pdo,'af_third_party_detections',['partida_id','detected_count','action_taken','detected_at','created_at'],'minor_usuario_id=?',[$subjectId],'id DESC'),
        'deletion_requests'=>afPrivacySelectRows($pdo,'af_deletion_requests',['privacy_request_id','scope_key','status','requested_at','completed_at','retention_reason','created_at','updated_at'],'requester_user_id=? OR subject_user_id=?',[$subjectId,$subjectId],'id DESC'),
        'guardian_links'=>afPrivacySelectRows($pdo,'af_guardian_minor_links',['id','guardian_usuario_id','minor_usuario_id','parentesco','status','aceito_em','verificado_em','family_confirmed_at','revogado_em','motivo_revogacao','criado_em','atualizado_em'],'guardian_usuario_id=? OR minor_usuario_id=?',[$subjectId,$subjectId],'id DESC'),
        'parental_consents'=>afPrivacySelectRows($pdo,'af_parental_consents',['id','link_id','guardian_usuario_id','minor_usuario_id','consent_version_id','status','confirmation_method','aceito_em','revogado_em','motivo_revogacao','criado_em','atualizado_em'],'guardian_usuario_id=? OR minor_usuario_id=?',[$subjectId,$subjectId],'id DESC'),
        'legal_acceptances'=>[],
        'controlled_event_participations'=>afPrivacySelectRows($pdo,'af_controlled_event_participants',['id','event_id','minor_user_id','guardian_link_id','alias_snapshot','status','authorized_at','checked_in_at','camera_zone_confirmed_at','completed_at','removed_at','removal_reason','created_at','updated_at'],'minor_user_id=?',[$subjectId],'id DESC'),
        'controlled_event_consents'=>afPrivacySelectRows($pdo,'af_controlled_event_consents',['id','participant_id','guardian_user_id','consent_version','account_participation','camera_processing','body_processing','event_ranking','guardian_notifications','public_stream','comments','private_messages','status','terms_hash','confirmation_method','granted_at','revoked_at','revocation_reason','created_at','updated_at'],'guardian_user_id=? OR participant_id IN (SELECT id FROM af_controlled_event_participants WHERE minor_user_id=?)',[$subjectId,$subjectId],'id DESC'),
        'controlled_event_checkins'=>afPrivacySelectRows($pdo,'af_controlled_event_checkins',['id','participant_id','checkin_type','camera_zone_confirmed','created_at'],'participant_id IN (SELECT id FROM af_controlled_event_participants WHERE minor_user_id=?)',[$subjectId],'id DESC'),
        'controlled_event_results'=>afPrivacySelectRows($pdo,'af_controlled_event_results',['id','event_id','participant_id','partida_id','score','rank_position','status','created_at','updated_at'],'participant_id IN (SELECT id FROM af_controlled_event_participants WHERE minor_user_id=?)',[$subjectId],'id DESC'),
        'privacy_requests'=>afPrivacySelectRows($pdo,'af_privacy_requests',['id','protocol','requester_user_id','subject_user_id','request_type','status','description','response_text','submitted_at','target_response_at','completed_at','cancelled_at','updated_at'],'requester_user_id=? OR subject_user_id=?',[$subjectId,$subjectId],'id DESC'),
    ];
    if(afPrivacyTableExists($pdo,'af_legal_acceptances')){
        try{
            $st=$pdo->prepare("SELECT a.accepted_at,a.source,a.ip_address,a.user_agent,d.document_key,d.version_key,d.title,d.content_hash FROM af_legal_acceptances a JOIN af_legal_documents d ON d.id=a.legal_document_id WHERE a.user_id=? ORDER BY a.id DESC");
            $st->execute([$subjectId]);$data['legal_acceptances']=$st->fetchAll()?:[];
        }catch(Throwable){}
    }
    return $data;
}

function afLogPrivacyExport(PDO $pdo, int $requesterId, int $subjectId, array $data, string $hash): string
{
    $protocol='AF-EXP-'.date('Ym').'-'.strtoupper(bin2hex(random_bytes(4)));
    if(afPrivacyTableExists($pdo,'af_data_exports')){
        $count=0;
        array_walk_recursive($data,static function()use(&$count){$count++;});
        $st=$pdo->prepare("INSERT INTO af_data_exports(protocol,requester_user_id,subject_user_id,format_key,file_hash,item_count,ip_address,user_agent,device_uuid,status,generated_at) VALUES (?,?,?,'json',?,?,?,?,?,'completed',NOW())");
        $st->execute([$protocol,$requesterId,$subjectId,$hash,$count,clientIp(),substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,1000),substr(deviceUuid(),0,64)]);
    }
    return $protocol;
}

function afPrivacyStatusLabel(string $status): string
{
    return afPrivacyRequestStatuses()[$status]??ucfirst(str_replace('_',' ',$status));
}
