<?php
/**
 * AURA FARMING — Etapa 25
 * Testes finais, checklist de liberação e release controlado para recursos de menores.
 */

if (!defined('STAGE25_RELEASE_VERSION')) define('STAGE25_RELEASE_VERSION', '25.0');

function afStage25TableExists(PDO $pdo, string $table): bool
{
    static $cache = [];
    if (isset($cache[$table])) return $cache[$table];
    try {
        $st = $pdo->prepare('SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? LIMIT 1');
        $st->execute([$table]);
        return $cache[$table] = (bool)$st->fetchColumn();
    } catch (Throwable $e) {
        return $cache[$table] = false;
    }
}

function afStage25ColumnExists(PDO $pdo, string $table, string $column): bool
{
    static $cache = [];
    $key = $table.'.'.$column;
    if (isset($cache[$key])) return $cache[$key];
    try {
        $st = $pdo->prepare('SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND COLUMN_NAME=? LIMIT 1');
        $st->execute([$table, $column]);
        return $cache[$key] = (bool)$st->fetchColumn();
    } catch (Throwable $e) {
        return $cache[$key] = false;
    }
}

function afStage25Json(array $data): ?string
{
    return $data ? json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
}

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

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

function afStage25SetConfig(PDO $pdo, string $key, string $value, string $type = 'string', string $description = ''): void
{
    try {
        if (!afStage25TableExists($pdo, 'af_configuracoes')) return;
        $st = $pdo->prepare('INSERT INTO af_configuracoes(chave,valor,tipo,publica,descricao) VALUES (?,?,?,0,?) ON DUPLICATE KEY UPDATE valor=VALUES(valor),tipo=VALUES(tipo),descricao=IF(VALUES(descricao)<>\'\',VALUES(descricao),descricao),atualizado_em=NOW()');
        $st->execute([$key, $value, $type, $description]);
        if (function_exists('afStage24AuditLog')) {
            afStage24AuditLog($pdo, 'stage25_config_updated', 'config', $key, [], ['valor'=>$value,'tipo'=>$type], ['severity'=>'info','source'=>'stage25']);
        }
    } catch (Throwable $e) {
        if (defined('APP_DEBUG') && APP_DEBUG) error_log('[AURA-STAGE25-CONFIG] '.$e->getMessage());
    }
}

function afStage25ReleaseModes(): array
{
    return [
        'adults_only' => [
            'label' => 'Somente adultos',
            'description' => 'Menores continuam bloqueados fora das rotinas essenciais.',
            'minor_scopes' => [],
        ],
        'controlled_event_pilot' => [
            'label' => 'Piloto: evento infantil controlado',
            'description' => 'Libera somente o modo de evento infantil com QR, check-in e supervisão.',
            'minor_scopes' => ['controlled_event'],
        ],
        'teen_16_17_pilot' => [
            'label' => 'Piloto: 16–17 protegido',
            'description' => 'Libera adolescentes de 16 e 17 anos nos recursos protegidos e autorizados pelo responsável.',
            'minor_scopes' => ['controlled_event','protected_minor'],
        ],
        'full_protected' => [
            'label' => 'Proteção completa para menores',
            'description' => 'Libera somente recursos seguros definidos pela política; recursos públicos de alto risco continuam bloqueados.',
            'minor_scopes' => ['controlled_event','protected_minor'],
        ],
    ];
}

function afStage25CurrentMode(PDO $pdo): string
{
    $mode = (string)afStage25Config($pdo, 'stage25_minor_release_mode', 'adults_only');
    return array_key_exists($mode, afStage25ReleaseModes()) ? $mode : 'adults_only';
}

function afStage25LatestRun(PDO $pdo): ?array
{
    if (!afStage25TableExists($pdo, 'af_stage25_test_runs')) return null;
    try {
        $st = $pdo->query('SELECT r.*,a.nome admin_nome FROM af_stage25_test_runs r LEFT JOIN af_admin_users a ON a.id=r.initiated_by_admin_id ORDER BY r.id DESC LIMIT 1');
        return $st ? ($st->fetch() ?: null) : null;
    } catch (Throwable $e) {
        return null;
    }
}

function afStage25LatestPassedRun(PDO $pdo): ?array
{
    if (!afStage25TableExists($pdo, 'af_stage25_test_runs')) return null;
    try {
        $st = $pdo->query("SELECT r.*,a.nome admin_nome FROM af_stage25_test_runs r LEFT JOIN af_admin_users a ON a.id=r.initiated_by_admin_id WHERE r.status='passed' ORDER BY r.id DESC LIMIT 1");
        return $st ? ($st->fetch() ?: null) : null;
    } catch (Throwable $e) {
        return null;
    }
}

function afStage25ActiveRelease(PDO $pdo): ?array
{
    if (!afStage25TableExists($pdo, 'af_stage25_release_approvals')) return null;
    try {
        $st = $pdo->query("SELECT r.*,a.nome admin_nome,t.status run_status,t.pass_rate FROM af_stage25_release_approvals r LEFT JOIN af_admin_users a ON a.id=r.approved_by_admin_id LEFT JOIN af_stage25_test_runs t ON t.id=r.checklist_run_id WHERE r.status='active' AND (r.expires_at IS NULL OR r.expires_at>NOW()) ORDER BY r.id DESC LIMIT 1");
        $row = $st ? ($st->fetch() ?: null) : null;
        if (!$row) return null;
        return $row;
    } catch (Throwable $e) {
        return null;
    }
}

function afStage25Count(PDO $pdo, string $sql): int
{
    try {
        $st = $pdo->query($sql);
        return $st ? (int)$st->fetchColumn() : 0;
    } catch (Throwable $e) {
        return 0;
    }
}

function afStage25CheckResult(string $key, string $category, string $severity, bool $passed, string $title, string $message, array $evidence = [], bool $warning = false): array
{
    $status = $passed ? 'passed' : ($warning ? 'warning' : 'failed');
    return compact('key','category','severity','status','passed','title','message','evidence');
}

function afStage25BuildChecklist(PDO $pdo, string $releaseMode = '', bool $includeLastRun = true): array
{
    $releaseMode = $releaseMode ?: afStage25CurrentMode($pdo);
    $results = [];

    $tables14_21 = [
        'af_user_age_profiles','af_guardian_accounts','af_guardian_minor_links','af_parental_consents',
        'af_minor_policy_versions','af_minor_policy_decision_logs','af_safe_camera_sessions','af_guardian_controls',
        'af_privacy_requests','af_data_protection_risk_assessments'
    ];
    $missing = array_values(array_filter($tables14_21, fn($t) => !afStage25TableExists($pdo, $t)));
    $results[] = afStage25CheckResult('tables_14_21','estrutura','critical', !$missing, 'Estrutura das Etapas 14 a 21', $missing ? 'Tabelas ausentes: '.implode(', ', $missing) : 'Tabelas principais encontradas.', ['missing'=>$missing]);

    $tables22 = ['af_safety_reports','af_user_blocks','af_moderation_actions','af_content_filters','af_minor_safety_incidents','af_live_comments','af_live_spectator_restrictions'];
    $missing = array_values(array_filter($tables22, fn($t) => !afStage25TableExists($pdo, $t)));
    $results[] = afStage25CheckResult('tables_stage22','estrutura','critical', !$missing, 'Etapa 22 instalada', $missing ? 'Tabelas de moderação ausentes: '.implode(', ', $missing) : 'Moderação e segurança social instaladas.', ['missing'=>$missing]);

    $tables23 = ['af_controlled_event_organizers','af_controlled_events','af_controlled_event_participants','af_controlled_event_consents','af_controlled_event_qr_tokens','af_controlled_event_checkins','af_controlled_event_supervision_sessions','af_controlled_event_results','af_controlled_event_purge_logs'];
    $missing = array_values(array_filter($tables23, fn($t) => !afStage25TableExists($pdo, $t)));
    $results[] = afStage25CheckResult('tables_stage23','estrutura','critical', !$missing, 'Etapa 23 instalada', $missing ? 'Tabelas de evento infantil ausentes: '.implode(', ', $missing) : 'Evento infantil controlado instalado.', ['missing'=>$missing]);

    $tables24 = ['af_audit_logs','af_security_alerts','af_user_risk_scores','af_api_access_logs','af_behavior_analysis','af_data_deletion_logs'];
    $missing = array_values(array_filter($tables24, fn($t) => !afStage25TableExists($pdo, $t)));
    $results[] = afStage25CheckResult('tables_stage24','estrutura','critical', !$missing, 'Etapa 24 instalada', $missing ? 'Tabelas de auditoria ausentes: '.implode(', ', $missing) : 'Auditoria e alertas instalados.', ['missing'=>$missing]);

    $tables25 = ['af_stage25_test_runs','af_stage25_test_results','af_stage25_release_approvals','af_stage25_pilot_events'];
    $missing = array_values(array_filter($tables25, fn($t) => !afStage25TableExists($pdo, $t)));
    $results[] = afStage25CheckResult('tables_stage25','estrutura','critical', !$missing, 'Estrutura da Etapa 25', $missing ? 'Tabelas da Etapa 25 ausentes: '.implode(', ', $missing) : 'Estrutura da liberação controlada instalada.', ['missing'=>$missing]);

    $salt = defined('SECURITY_HASH_SALT') ? (string)SECURITY_HASH_SALT : '';
    $defaultSalts = ['troque-esta-chave-em-producao','COLOQUE_AQUI_UMA_CHAVE_ALEATORIA_COM_64_CARACTERES','aura-admin','aura-stage24'];
    $saltOk = strlen($salt) >= 64 && !in_array($salt, $defaultSalts, true);
    $results[] = afStage25CheckResult('security_hash_salt','seguranca','critical', $saltOk, 'Chave de segurança real', $saltOk ? 'SECURITY_HASH_SALT possui tamanho adequado e não é placeholder.' : 'Configure uma chave real de pelo menos 64 caracteres em config/config.local.php.', ['length'=>strlen($salt)]);

    $controllerOk = false; $missingController = [];
    try {
        $controller = function_exists('afPrivacyController') ? afPrivacyController($pdo) : [];
        foreach(['name','document','address','contact_email'] as $field) {
            if (trim((string)($controller[$field] ?? '')) === '') $missingController[] = $field;
        }
        $controllerOk = !$missingController;
    } catch (Throwable $e) { $missingController = ['controller_unavailable']; }
    $results[] = afStage25CheckResult('controller_data','legal','critical', $controllerOk, 'Dados do controlador', $controllerOk ? 'Dados do controlador preenchidos.' : 'Complete os dados do controlador no painel de privacidade: '.implode(', ', $missingController), ['missing'=>$missingController]);

    $risk = function_exists('afPrivacyMinorReleaseReady') ? afPrivacyMinorReleaseReady($pdo) : ['ready'=>false,'code'=>'FUNCTION_MISSING'];
    $results[] = afStage25CheckResult('risk_assessment_legal','legal','critical', !empty($risk['ready']), 'Relatório de riscos aprovado', !empty($risk['ready']) ? 'Relatório de riscos e revisão jurídica aprovados.' : 'Liberação bloqueada: '.(string)($risk['code'] ?? 'RISK_REQUIRED'), ['code'=>$risk['code'] ?? null]);

    $safeCamera = (bool)afStage25Config($pdo, 'minor_safe_camera_ready', false);
    $results[] = afStage25CheckResult('safe_camera_ready','tecnico','critical', $safeCamera, 'Câmera segura ativa', $safeCamera ? 'Gate minor_safe_camera_ready está ativo.' : 'Ative e valide a câmera segura antes de liberar menores.', ['config'=>'minor_safe_camera_ready']);

    $lightingOk = afStage25ColumnExists($pdo, 'af_safe_camera_sessions', 'lighting_ok') || afStage25ColumnExists($pdo, 'af_partidas', 'lighting_ok');
    $results[] = afStage25CheckResult('lighting_detection_schema','tecnico','high', $lightingOk, 'Registro de iluminação', $lightingOk ? 'Campos de iluminação encontrados.' : 'Campos de iluminação não foram encontrados para auditoria.', ['safe_camera_lighting_ok'=>afStage25ColumnExists($pdo,'af_safe_camera_sessions','lighting_ok'),'partidas_lighting_ok'=>afStage25ColumnExists($pdo,'af_partidas','lighting_ok')]);

    $farmGate = (bool)afStage25Config($pdo, 'minor_individual_farm_enabled', false);
    $battleGate = (bool)afStage25Config($pdo, 'minor_battle_enabled', false);
    $rankingGate = (bool)afStage25Config($pdo, 'minor_ranking_alias_enabled', false);
    $featureGatesOk = $releaseMode === 'controlled_event_pilot' || $releaseMode === 'adults_only' || ($farmGate && $battleGate && $rankingGate);
    $results[] = afStage25CheckResult('minor_feature_gates','tecnico','high', $featureGatesOk, 'Gates de recursos protegidos', $featureGatesOk ? 'Gates compatíveis com o modo de liberação escolhido.' : 'Para piloto 16–17, ative farm, battle e ranking alias protegidos.', ['farm'=>$farmGate,'battle'=>$battleGate,'ranking_alias'=>$rankingGate,'release_mode'=>$releaseMode]);

    $criticalOpen = afStage25TableExists($pdo, 'af_security_alerts') ? afStage25Count($pdo, "SELECT COUNT(*) FROM af_security_alerts WHERE severity='critical' AND status IN ('open','triage','in_review')") : 999;
    $highOpen = afStage25TableExists($pdo, 'af_security_alerts') ? afStage25Count($pdo, "SELECT COUNT(*) FROM af_security_alerts WHERE severity='high' AND status IN ('open','triage','in_review')") : 999;
    $results[] = afStage25CheckResult('no_open_critical_alerts','operacao','critical', $criticalOpen === 0, 'Sem alerta crítico aberto', $criticalOpen === 0 ? 'Nenhum alerta crítico aberto.' : $criticalOpen.' alerta(s) crítico(s) aberto(s).', ['critical_open'=>$criticalOpen]);
    $results[] = afStage25CheckResult('open_high_alerts_reviewed','operacao','high', $highOpen === 0, 'Alertas altos tratados', $highOpen === 0 ? 'Nenhum alerta alto aberto.' : $highOpen.' alerta(s) alto(s) ainda aberto(s).', ['high_open'=>$highOpen], $highOpen > 0);

    $apiDenied = afStage25TableExists($pdo, 'af_api_access_logs') ? afStage25Count($pdo, "SELECT COUNT(*) FROM af_api_access_logs WHERE allowed=0 AND created_at>=DATE_SUB(NOW(),INTERVAL 24 HOUR)") : 0;
    $results[] = afStage25CheckResult('api_denials_review','operacao','medium', $apiDenied < 50, 'Bloqueios de API revisados', $apiDenied < 50 ? 'Volume de bloqueios de API está dentro do limite operacional.' : 'Há muitos bloqueios de API nas últimas 24h. Revise antes da liberação.', ['api_denied_24h'=>$apiDenied], $apiDenied >= 50);

    $deletionReady = afStage25TableExists($pdo, 'af_data_deletion_logs') && function_exists('afPrivacyExecuteDeletionRequest');
    $results[] = afStage25CheckResult('deletion_execution_ready','privacidade','critical', $deletionReady, 'Exclusão técnica pronta', $deletionReady ? 'Rotina de exclusão técnica e logs disponíveis.' : 'A rotina de execução de exclusão da Etapa 21 não está disponível.', ['table'=>afStage25TableExists($pdo,'af_data_deletion_logs'),'function'=>function_exists('afPrivacyExecuteDeletionRequest')]);

    $eventRetention = is_file((defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__)) . '/cron/eventos-infantis-retencao.php');
    $results[] = afStage25CheckResult('event_retention_cron_file','privacidade','high', $eventRetention, 'Arquivo de retenção de eventos', $eventRetention ? 'Cron de retenção da Etapa 23 encontrado.' : 'Arquivo cron/eventos-infantis-retencao.php não encontrado.', ['file'=>$eventRetention], !$eventRetention);

    $stage24Cron = is_file((defined('ROOT_PATH') ? ROOT_PATH : dirname(__DIR__)) . '/cron/etapa24-auditoria-rotina.php');
    $results[] = afStage25CheckResult('stage24_audit_cron_file','operacao','medium', $stage24Cron, 'Arquivo de rotina da Etapa 24', $stage24Cron ? 'Cron de auditoria encontrado.' : 'Arquivo cron/etapa24-auditoria-rotina.php não encontrado.', ['file'=>$stage24Cron], !$stage24Cron);

    $latestPassed = $includeLastRun ? afStage25LatestPassedRun($pdo) : null;
    $lastRunOk = !$includeLastRun || (bool)$latestPassed;
    $results[] = afStage25CheckResult('last_stage25_run_passed','liberacao','critical', $lastRunOk, 'Última bateria de testes aprovada', $lastRunOk ? 'Existe bateria de testes aprovada.' : 'Execute e aprove a bateria de testes da Etapa 25 antes da liberação.', ['last_passed_run_id'=>$latestPassed['id'] ?? null]);

    return $results;
}

function afStage25ChecklistSummary(array $results): array
{
    $total = count($results);
    $passed = 0; $warnings = 0; $failed = 0; $criticalFailures = 0; $highFailures = 0;
    foreach ($results as $r) {
        if (($r['status'] ?? '') === 'passed') $passed++;
        elseif (($r['status'] ?? '') === 'warning') $warnings++;
        else $failed++;
        if (($r['status'] ?? '') === 'failed' && ($r['severity'] ?? '') === 'critical') $criticalFailures++;
        if (($r['status'] ?? '') === 'failed' && in_array(($r['severity'] ?? ''), ['critical','high'], true)) $highFailures++;
    }
    return [
        'total'=>$total,
        'passed'=>$passed,
        'warnings'=>$warnings,
        'failed'=>$failed,
        'critical_failures'=>$criticalFailures,
        'high_failures'=>$highFailures,
        'pass_rate'=>$total > 0 ? round(($passed / $total) * 100, 2) : 0.0,
        'status'=>$criticalFailures === 0 && $failed === 0 ? 'passed' : 'failed',
    ];
}

function afStage25RunAutomatedChecklist(PDO $pdo, int $adminId = 0, string $runType = 'manual', string $releaseMode = ''): array
{
    if (!afStage25TableExists($pdo, 'af_stage25_test_runs') || !afStage25TableExists($pdo, 'af_stage25_test_results')) {
        throw new RuntimeException('Importe o SQL da Etapa 25 antes de rodar os testes.');
    }
    $releaseMode = $releaseMode ?: afStage25CurrentMode($pdo);
    $pdo->prepare("INSERT INTO af_stage25_test_runs(run_uuid,run_type,status,release_mode,initiated_by_admin_id,started_at,created_at,updated_at) VALUES (?,?, 'running', ?, ?, NOW(), NOW(), NOW())")
        ->execute([afStage25Uuid(), substr($runType,0,40), $releaseMode, $adminId ?: null]);
    $runId = (int)$pdo->lastInsertId();

    $results = afStage25BuildChecklist($pdo, $releaseMode, false);
    $summary = afStage25ChecklistSummary($results);
    foreach ($results as $r) {
        $pdo->prepare('INSERT INTO af_stage25_test_results(run_id,check_key,category,severity,status,title,message,evidence_json,created_at) VALUES (?,?,?,?,?,?,?,?,NOW())')
            ->execute([$runId, substr((string)$r['key'],0,120), substr((string)$r['category'],0,80), (string)$r['severity'], (string)$r['status'], substr((string)$r['title'],0,180), (string)$r['message'], afStage25Json($r['evidence'] ?? [])]);
    }
    $status = $summary['critical_failures'] === 0 && $summary['failed'] === 0 ? 'passed' : 'failed';
    $pdo->prepare('UPDATE af_stage25_test_runs SET status=?,pass_rate=?,critical_failures=?,high_failures=?,results_json=?,finished_at=NOW(),updated_at=NOW() WHERE id=?')
        ->execute([$status, $summary['pass_rate'], $summary['critical_failures'], $summary['high_failures'], afStage25Json(['summary'=>$summary,'results'=>$results]), $runId]);

    if (function_exists('afStage24AuditLog')) {
        afStage24AuditLog($pdo, 'stage25_checklist_run', 'stage25_test_run', $runId, [], ['status'=>$status,'pass_rate'=>$summary['pass_rate'],'release_mode'=>$releaseMode], ['severity'=>$status==='passed'?'info':'high','source'=>'stage25']);
    }
    if ($status !== 'passed' && function_exists('afStage24SecurityAlert')) {
        afStage24SecurityAlert($pdo, 'stage25_checklist_failed', 'high', 'Etapa 25 encontrou falhas de liberação', 'A liberação de menores continua bloqueada até a correção dos itens reprovados.', null, null, 'stage25', 'stage25_test_run', $runId, ['summary'=>$summary], 12);
    }
    return ['run_id'=>$runId,'status'=>$status,'summary'=>$summary,'results'=>$results];
}

function afStage25ReleaseCanApprove(PDO $pdo, string $mode): array
{
    $modes = afStage25ReleaseModes();
    if (!isset($modes[$mode])) return ['allowed'=>false,'code'=>'INVALID_MODE','message'=>'Modo de liberação inválido.'];
    if ($mode === 'adults_only') return ['allowed'=>true,'code'=>'ADULTS_ONLY','message'=>'Modo seguro padrão.'];
    $run = afStage25LatestPassedRun($pdo);
    if (!$run) return ['allowed'=>false,'code'=>'CHECKLIST_REQUIRED','message'=>'Execute uma bateria de testes aprovada antes de liberar.'];
    $results = afStage25BuildChecklist($pdo, $mode, true);
    $summary = afStage25ChecklistSummary($results);
    if ($summary['critical_failures'] > 0 || $summary['failed'] > 0) {
        return ['allowed'=>false,'code'=>'CHECKLIST_FAILED','message'=>'Ainda existem itens reprovados na checklist final.','summary'=>$summary,'results'=>$results];
    }
    if ($mode === 'full_protected' && !afStage25Config($pdo, 'stage25_allow_full_protected_release', false)) {
        return ['allowed'=>false,'code'=>'FULL_RELEASE_DISABLED','message'=>'A liberação completa permanece desativada por configuração. Use piloto controlado ou 16–17 protegido.'];
    }
    return ['allowed'=>true,'code'=>'READY','message'=>'Modo apto para liberação controlada.','run'=>$run,'summary'=>$summary,'results'=>$results];
}

function afStage25ApproveRelease(PDO $pdo, int $adminId, string $mode, string $note = '', int $days = 30): array
{
    if (!afStage25TableExists($pdo, 'af_stage25_release_approvals')) throw new RuntimeException('Importe o SQL da Etapa 25.');
    $can = afStage25ReleaseCanApprove($pdo, $mode);
    if (empty($can['allowed'])) throw new RuntimeException((string)$can['message']);
    $run = afStage25LatestPassedRun($pdo);
    $expires = $mode === 'adults_only' ? null : (new DateTimeImmutable('+'.max(1,min(365,$days)).' days'))->format('Y-m-d H:i:s');
    $pdo->beginTransaction();
    try {
        $pdo->prepare("UPDATE af_stage25_release_approvals SET status='revoked',revoked_by_admin_id=?,revoked_at=NOW(),revoked_reason='Substituído por nova liberação' WHERE status='active'")->execute([$adminId ?: null]);
        $pdo->prepare("INSERT INTO af_stage25_release_approvals(approval_uuid,release_mode,status,approved_by_admin_id,approved_at,expires_at,note,checklist_run_id,created_at,updated_at) VALUES (?,?, 'active', ?, NOW(), ?, ?, ?, NOW(), NOW())")
            ->execute([afStage25Uuid(), $mode, $adminId ?: null, $expires, mb_substr($note,0,2000), $run['id'] ?? null]);
        $approvalId = (int)$pdo->lastInsertId();
        afStage25SetConfig($pdo, 'stage25_minor_release_mode', $mode, 'string', 'Modo de liberação controlada da Etapa 25.');
        afStage25SetConfig($pdo, 'stage25_last_release_at', date('Y-m-d H:i:s'), 'string', 'Data/hora da última liberação controlada.');
        $pdo->commit();
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
    if (function_exists('afStage24AuditLog')) {
        afStage24AuditLog($pdo, 'stage25_release_approved', 'stage25_release', $approvalId, [], ['mode'=>$mode,'expires_at'=>$expires], ['severity'=>'high','source'=>'stage25']);
    }
    return ['approval_id'=>$approvalId,'mode'=>$mode,'expires_at'=>$expires];
}

function afStage25RevokeRelease(PDO $pdo, int $adminId, string $reason = ''): void
{
    if (!afStage25TableExists($pdo, 'af_stage25_release_approvals')) return;
    $pdo->prepare("UPDATE af_stage25_release_approvals SET status='revoked',revoked_by_admin_id=?,revoked_at=NOW(),revoked_reason=? WHERE status='active'")
        ->execute([$adminId ?: null, mb_substr($reason ?: 'Revogado pelo administrador',0,1000)]);
    afStage25SetConfig($pdo, 'stage25_minor_release_mode', 'adults_only', 'string', 'Modo de liberação controlada da Etapa 25.');
    if (function_exists('afStage24AuditLog')) {
        afStage24AuditLog($pdo, 'stage25_release_revoked', 'stage25_release', null, [], ['reason'=>$reason], ['severity'=>'critical','source'=>'stage25']);
    }
    if (function_exists('afStage24SecurityAlert')) {
        afStage24SecurityAlert($pdo, 'stage25_release_revoked', 'high', 'Liberação de menores revogada', $reason ?: 'A liberação controlada foi revogada e o sistema voltou para somente adultos.', null, null, 'stage25', 'stage25_release', null, ['reason'=>$reason], 10);
    }
}

function afStage25ExpireOldReleases(PDO $pdo): int
{
    if (!afStage25TableExists($pdo, 'af_stage25_release_approvals')) return 0;
    $st = $pdo->prepare("UPDATE af_stage25_release_approvals SET status='expired',revoked_reason='Expirado automaticamente',updated_at=NOW() WHERE status='active' AND expires_at IS NOT NULL AND expires_at<=NOW()");
    $st->execute();
    $count = $st->rowCount();
    if ($count > 0) afStage25SetConfig($pdo, 'stage25_minor_release_mode', 'adults_only', 'string', 'Modo de liberação controlada da Etapa 25.');
    return $count;
}

function afStage25MinorReleaseReady(PDO $pdo, string $scope = 'protected_minor', string $feature = ''): array
{
    if (!afStage25Config($pdo, 'stage25_enabled', true)) {
        return ['ready'=>false,'code'=>'STAGE25_DISABLED','message'=>'A Etapa 25 está desativada.'];
    }
    $base = function_exists('afPrivacyMinorReleaseReady') ? afPrivacyMinorReleaseReady($pdo) : ['ready'=>false,'code'=>'RISK_GATE_UNAVAILABLE'];
    if (empty($base['ready'])) {
        return ['ready'=>false,'code'=>(string)($base['code'] ?? 'RISK_GATE_REQUIRED'),'message'=>'A revisão jurídica e o relatório de riscos ainda bloqueiam a liberação.','base'=>$base];
    }
    afStage25ExpireOldReleases($pdo);
    $active = afStage25ActiveRelease($pdo);
    if (!$active) {
        return ['ready'=>false,'code'=>'STAGE25_RELEASE_NOT_APPROVED','message'=>'A Etapa 25 ainda não aprovou nenhuma liberação controlada.','mode'=>afStage25CurrentMode($pdo),'base'=>$base];
    }
    $mode = (string)$active['release_mode'];
    $modes = afStage25ReleaseModes();
    $allowedScopes = $modes[$mode]['minor_scopes'] ?? [];
    if (!in_array($scope, $allowedScopes, true)) {
        return ['ready'=>false,'code'=>'STAGE25_SCOPE_BLOCKED','message'=>'O modo atual não libera este tipo de recurso para menores.','mode'=>$mode,'scope'=>$scope,'feature'=>$feature,'base'=>$base];
    }
    return ['ready'=>true,'code'=>'STAGE25_RELEASE_APPROVED','message'=>'Liberação controlada aprovada pela Etapa 25.','mode'=>$mode,'scope'=>$scope,'feature'=>$feature,'approval'=>$active,'base'=>$base];
}

function afStage25Metrics(PDO $pdo): array
{
    afStage25ExpireOldReleases($pdo);
    $latest = afStage25LatestRun($pdo);
    $passed = afStage25LatestPassedRun($pdo);
    $active = afStage25ActiveRelease($pdo);
    $critical = afStage25TableExists($pdo,'af_security_alerts') ? afStage25Count($pdo,"SELECT COUNT(*) FROM af_security_alerts WHERE severity='critical' AND status IN ('open','triage','in_review')") : 0;
    $minors = afStage25TableExists($pdo,'af_user_age_profiles') ? afStage25Count($pdo,"SELECT COUNT(*) FROM af_user_age_profiles WHERE tipo_conta='minor'") : 0;
    $events = afStage25TableExists($pdo,'af_controlled_events') ? afStage25Count($pdo,"SELECT COUNT(*) FROM af_controlled_events WHERE status IN ('approved','active','scheduled')") : 0;
    return [
        'mode'=>afStage25CurrentMode($pdo),
        'active_release'=>$active,
        'latest_run'=>$latest,
        'latest_passed_run'=>$passed,
        'critical_alerts'=>$critical,
        'minor_accounts'=>$minors,
        'active_events'=>$events,
    ];
}

function afStage25LogPilotEvent(PDO $pdo, string $eventType, ?int $userId = null, ?string $entityType = null, int|string|null $entityId = null, string $status = 'recorded', array $details = []): void
{
    if (!afStage25TableExists($pdo, 'af_stage25_pilot_events')) return;
    try {
        $pdo->prepare('INSERT INTO af_stage25_pilot_events(event_type,user_id,entity_type,entity_id,status,details_json,ip_hash,user_agent_hash,device_uuid,created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())')
            ->execute([substr($eventType,0,100), $userId ?: null, $entityType ?: null, $entityId !== null ? substr((string)$entityId,0,80) : null, substr($status,0,40), afStage25Json($details), function_exists('afStage24IpHash')?afStage24IpHash():null, function_exists('afStage24UaHash')?afStage24UaHash():null, function_exists('afStage24DeviceUuid')?afStage24DeviceUuid():null]);
    } catch (Throwable $e) {
        if (defined('APP_DEBUG') && APP_DEBUG) error_log('[AURA-STAGE25-PILOT] '.$e->getMessage());
    }
}
