<?php

declare(strict_types=1);

/**
 * DentalCare v0.8.0.1 production release-candidate gates.
 *
 * Approval is evidence-bound. It becomes invalid whenever the uploaded release
 * changes, a production blocker appears, required evidence becomes stale, role
 * prerequisites fail, the reviewed monitor state changes, or the approval used
 * to create a production package is no longer the current approval.
 */

require_once DENTAL_ROOT . '/app/production_readiness.php';
require_once DENTAL_ROOT . '/app/production_validation.php';

function dental_release_candidate_required_since(): int
{
    $raw = (string) setting_get('system', 'release_candidate_required_since', '');
    $timestamp = $raw !== '' ? strtotime($raw) : false;
    if ($timestamp !== false && $timestamp > 0) return $timestamp;

    $fallback = (string) setting_get('system', 'last_update_completed_at', '');
    $timestamp = $fallback !== '' ? strtotime($fallback) : false;
    return $timestamp !== false && $timestamp > 0 ? $timestamp : 0;
}

function dental_release_candidate_emergency_file_names(): array
{
    return [
        'emergency_finalize_077x_update.php',
        'emergency_diagnostic.php',
        'emergency_bootstrap_probe.php',
        'dc_bootstrap_probe_20260710.php',
    ];
}

function dental_release_candidate_emergency_files(): array
{
    $files = [];
    foreach (dental_release_candidate_emergency_file_names() as $name) {
        $path = DENTAL_ROOT . '/' . $name;
        if (is_file($path)) $files[] = $name;
    }
    foreach (glob(DENTAL_ROOT . '/dc_bootstrap_probe_*.php') ?: [] as $path) {
        $name = basename($path);
        if (!in_array($name, $files, true)) $files[] = $name;
    }
    sort($files);
    return $files;
}

function dental_release_candidate_cleanup_emergency_files(): array
{
    $removed = [];
    $failed = [];
    foreach (dental_release_candidate_emergency_files() as $name) {
        $path = DENTAL_ROOT . '/' . basename($name);
        if (!is_file($path)) continue;
        if (@unlink($path)) $removed[] = $name;
        else $failed[] = $name;
    }
    return ['removed' => $removed, 'failed' => $failed];
}

function dental_release_candidate_backup_rows(PDO $pdo): array
{
    if (!schema_table_exists(table('backup_logs'))) return [];

    $available = ['id', 'file_name', 'created_at'];
    foreach (['status', 'checksum_sha256', 'verified_at', 'backup_type', 'file_size'] as $column) {
        if (schema_column_exists(table('backup_logs'), $column)) $available[] = $column;
    }

    try {
        $stmt = $pdo->query(
            'SELECT ' . implode(',', array_map(static fn(string $column): string => '`' . $column . '`', $available))
            . ' FROM `' . table('backup_logs') . '` ORDER BY `created_at` DESC,`id` DESC LIMIT 50'
        );
        return $stmt->fetchAll() ?: [];
    } catch (Throwable $exception) {
        if (function_exists('dental_runtime_write_log')) {
            dental_runtime_write_log('Release Candidate backup evidence query failed: ' . $exception->getMessage());
        }
        return [];
    }
}

function dental_release_candidate_backup_evidence(PDO $pdo, int $requiredSince): array
{
    return dental_production_backup_evidence($pdo, $requiredSince, true);
}

function dental_release_candidate_timestamp_ready(string $raw, int $requiredSince): bool
{
    $timestamp = $raw !== '' ? (strtotime($raw) ?: 0) : 0;
    return $timestamp > 0
        && $timestamp <= time() + 300
        && ($requiredSince === 0 || $timestamp >= $requiredSince);
}

function dental_release_candidate_manual_flow_evidence(): array
{
    $default = [
        'evidence_id' => '',
        'version' => '',
        'tested_at' => '',
        'tested_by' => '',
        'tested_by_user_id' => 0,
        'scope' => '',
        'note' => '',
    ];
    $raw = (string) setting_get('production_acceptance', 'manual_flow_evidence', '');
    if ($raw === '') return $default;
    $decoded = json_decode($raw, true);
    return is_array($decoded) ? array_merge($default, $decoded) : $default;
}

function dental_release_candidate_record_manual_flow(string $confirmation, string $scope, string $note): array
{
    $expected = 'FLOW TESTED ' . app_release_version();
    if (!hash_equals($expected, trim($confirmation))) {
        throw new RuntimeException('Type ' . $expected . ' exactly after completing the end-to-end browser test.');
    }
    if (!in_array($scope, ['appointment', 'walk_in', 'both'], true)) {
        throw new RuntimeException('Choose whether the tested flow used an appointment, walk-in, or both.');
    }
    if (mb_strlen(trim($note)) < 20) {
        throw new RuntimeException('Add a detailed flow note of at least 20 characters describing the patient path, payment outcome, and checkout result.');
    }
    $user = current_user() ?: [];
    $record = [
        'evidence_id' => bin2hex(random_bytes(12)),
        'version' => app_release_version(),
        'tested_at' => date('c'),
        'tested_by' => (string) ($user['name'] ?? $user['email'] ?? 'Superuser'),
        'tested_by_user_id' => (int) ($user['id'] ?? 0),
        'scope' => $scope,
        'note' => mb_substr(trim($note), 0, 1500),
    ];
    setting_set('production_acceptance', 'manual_flow_evidence', json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    setting_set('system', 'manual_role_flow_reviewed_at', (string) $record['tested_at']);
    setting_set('system', 'manual_role_flow_reviewed_by', (string) $record['tested_by_user_id']);
    return $record;
}

function dental_release_candidate_restore_evidence(): array
{
    $default = [
        'evidence_id' => '',
        'version' => '',
        'tested_at' => '',
        'tested_by' => '',
        'tested_by_user_id' => 0,
        'backup_id' => 0,
        'backup_checksum_sha256' => '',
        'environment' => '',
        'note' => '',
    ];
    $raw = (string) setting_get('production_acceptance', 'restore_test_evidence', '');
    if ($raw === '') return $default;
    $decoded = json_decode($raw, true);
    return is_array($decoded) ? array_merge($default, $decoded) : $default;
}

function dental_release_candidate_backup_by_id(PDO $pdo, int $backupId): ?array
{
    if ($backupId <= 0 || !schema_table_exists(table('backup_logs'))) return null;
    $columns = ['id', 'file_name', 'created_at'];
    foreach (['status', 'checksum_sha256', 'verified_at', 'backup_type', 'file_size'] as $column) {
        if (schema_column_exists(table('backup_logs'), $column)) $columns[] = $column;
    }
    $stmt = $pdo->prepare(
        'SELECT ' . implode(',', array_map(static fn(string $column): string => '`' . $column . '`', $columns))
        . ' FROM `' . table('backup_logs') . '` WHERE `id`=? LIMIT 1'
    );
    $stmt->execute([$backupId]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function dental_release_candidate_record_restore_test(
    PDO $pdo,
    int $backupId,
    string $environment,
    string $confirmation,
    string $note
): array {
    $expected = 'RESTORE TESTED ' . app_release_version();
    if (!hash_equals($expected, trim($confirmation))) {
        throw new RuntimeException('Type ' . $expected . ' exactly after completing the restore or recovery drill.');
    }
    $allowedEnvironments = ['staging_restore', 'clean_server_restore', 'documented_recovery_drill'];
    if (!in_array($environment, $allowedEnvironments, true)) {
        throw new RuntimeException('Choose the environment used for the restore or recovery drill.');
    }
    if (mb_strlen(trim($note)) < 20) {
        throw new RuntimeException('Add a recovery note of at least 20 characters describing what was restored, checked, and confirmed.');
    }

    $requiredSince = dental_release_candidate_required_since();
    $backup = dental_release_candidate_backup_by_id($pdo, $backupId);
    if (!$backup) throw new RuntimeException('Choose an available managed backup record.');
    $evaluation = dental_production_backup_row_evaluation($backup, $requiredSince, true);
    if (empty($evaluation['ready'])) {
        throw new RuntimeException('The selected backup is not eligible for the current release: ' . implode('; ', (array) ($evaluation['reasons'] ?? ['verification failed'])) . '.');
    }

    $user = current_user() ?: [];
    $record = [
        'evidence_id' => bin2hex(random_bytes(12)),
        'version' => app_release_version(),
        'tested_at' => date('c'),
        'tested_by' => (string) ($user['name'] ?? $user['email'] ?? 'Superuser'),
        'tested_by_user_id' => (int) ($user['id'] ?? 0),
        'backup_id' => (int) ($backup['id'] ?? 0),
        'backup_checksum_sha256' => (string) ($backup['checksum_sha256'] ?? ''),
        'environment' => $environment,
        'note' => mb_substr(trim($note), 0, 1500),
    ];
    setting_set('production_acceptance', 'restore_test_evidence', json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    setting_set('system', 'restore_tested_at', (string) $record['tested_at']);
    setting_set('system', 'restore_tested_by', (string) $record['tested_by_user_id']);
    return $record;
}

function dental_release_candidate_production_fingerprint(array $production): string
{
    $rows = [];
    foreach ((array) ($production['checks'] ?? []) as $check) {
        $key = (string) ($check['key'] ?? '');
        if ($key === '') continue;
        $rows[$key] = (string) ($check['status'] ?? 'review');
    }
    ksort($rows);
    return hash('sha256', json_encode($rows, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '');
}

function dental_release_candidate_record_monitor_review(PDO $pdo): array
{
    $production = dental_production_health_snapshot($pdo);
    $blocked = (int) ($production['counts']['blocked'] ?? 1);
    if ($blocked > 0) {
        throw new RuntimeException('Resolve all blocked Production Monitor checks before recording the formal review.');
    }

    $reviewedAt = date('c');
    $fingerprint = dental_release_candidate_production_fingerprint($production);
    setting_set('production_acceptance', 'last_monitor_reviewed_at', $reviewedAt);
    setting_set('production_acceptance', 'last_monitor_reviewed_by', (string) (current_user()['id'] ?? 0));
    setting_set('production_acceptance', 'last_monitor_review_fingerprint', $fingerprint);
    setting_set('production_acceptance', 'last_monitor_review_version', app_release_version());
    setting_set('production_acceptance', 'last_monitor_review_evidence_id', bin2hex(random_bytes(12)));

    return ['reviewed_at' => $reviewedAt, 'fingerprint' => $fingerprint];
}

function dental_release_candidate_role_summary(PDO $pdo): array
{
    $requiredSince = dental_release_candidate_required_since();
    $prerequisites = dental_production_role_prerequisites($pdo);
    $rows = [];
    $passed = 0;

    foreach (dental_production_role_codes() as $code => $label) {
        $signoff = dental_production_role_signoff($code);
        $recordedAt = (string) ($signoff['recorded_at'] ?? '');
        $recordedTimestamp = $recordedAt !== '' ? (strtotime($recordedAt) ?: 0) : 0;
        $timeCurrent = $recordedTimestamp > 0
            && $recordedTimestamp <= time() + 300
            && ($requiredSince === 0 || $recordedTimestamp >= $requiredSince);
        $versionCurrent = (string) ($signoff['version'] ?? '') === app_release_version();
        $evidenceIdReady = trim((string) ($signoff['evidence_id'] ?? '')) !== '';
        $prerequisite = (array) ($prerequisites[$code] ?? ['ready' => false, 'detail' => 'Role prerequisites could not be verified.']);
        $prerequisiteReady = !empty($prerequisite['ready']);
        $currentPrerequisiteFingerprint = dental_production_role_prerequisite_fingerprint($prerequisite);
        $storedPrerequisiteFingerprint = trim((string) ($signoff['prerequisite_fingerprint'] ?? ''));
        $prerequisiteEvidenceCurrent = $storedPrerequisiteFingerprint !== ''
            && hash_equals($storedPrerequisiteFingerprint, $currentPrerequisiteFingerprint);
        $current = $timeCurrent && $versionCurrent && $evidenceIdReady && $prerequisiteEvidenceCurrent;
        $ready = (string) ($signoff['status'] ?? '') === 'passed' && $current && $prerequisiteReady;
        if ($ready) $passed++;

        $rows[$code] = [
            'label' => $label,
            'ready' => $ready,
            'current' => $current,
            'status' => (string) ($signoff['status'] ?? 'not_tested'),
            'recorded_at' => $recordedAt,
            'version_current' => $versionCurrent,
            'evidence_id_ready' => $evidenceIdReady,
            'prerequisite_ready' => $prerequisiteReady,
            'prerequisite_evidence_current' => $prerequisiteEvidenceCurrent,
            'prerequisite_detail' => (string) ($prerequisite['detail'] ?? ''),
        ];
    }

    return [
        'rows' => $rows,
        'passed' => $passed,
        'total' => count($rows),
        'ready' => $passed === count($rows),
    ];
}

function dental_release_candidate_gate(
    string $key,
    string $title,
    bool $ready,
    string $detail,
    string $metric = '',
    string $href = '',
    string $actionLabel = 'Complete'
): array {
    return compact('key', 'title', 'ready', 'detail', 'metric', 'href', 'actionLabel');
}

function dental_release_candidate_smtp_evidence(PDO $pdo, int $requiredSince): array
{
    $default = ['ready'=>false,'tested_at'=>'','driver'=>'','message'=>'No successful live test delivery is recorded for this release.'];
    if (!schema_table_exists(table('smtp_diagnostics'))) return $default;
    try {
        $sql = 'SELECT test_type,status,host,encryption,message,created_at FROM `' . table('smtp_diagnostics') . '` '
            . 'WHERE status="success" AND test_type="delivery"';
        $args = [];
        if ($requiredSince > 0) {
            $sql .= ' AND created_at>=?';
            $args[] = date('Y-m-d H:i:s', $requiredSince);
        }
        $sql .= ' ORDER BY created_at DESC,id DESC LIMIT 1';
        $stmt = $pdo->prepare($sql);
        $stmt->execute($args);
        $row = $stmt->fetch();
        if (!$row) return $default;
        $timestamp = strtotime((string)$row['created_at']) ?: 0;
        if ($timestamp <= 0 || $timestamp > time() + 300) return $default;
        return [
            'ready' => true,
            'tested_at' => (string)$row['created_at'],
            'driver' => (string)($row['host'] ?? ''),
            'message' => (string)($row['message'] ?? 'Live test delivery succeeded.'),
        ];
    } catch (Throwable $exception) {
        return array_merge($default, ['message'=>'Email test evidence could not be read: ' . $exception->getMessage()]);
    }
}

function dental_release_candidate_snapshot(PDO $pdo): array
{
    $release = app_release_version();
    $installed = installed_application_version();
    $requiredSince = dental_release_candidate_required_since();
    $automated = dental_automated_validation_snapshot($pdo);

    $restoreEvidence = dental_release_candidate_restore_evidence();
    $restoreRaw = (string)($restoreEvidence['tested_at'] ?? '');
    $restoreTimestamp = $restoreRaw !== '' ? (strtotime($restoreRaw) ?: 0) : 0;
    $restoreBackup = dental_release_candidate_backup_by_id($pdo, (int)($restoreEvidence['backup_id'] ?? 0));
    $restoreBackupEvaluation = $restoreBackup ? dental_production_backup_row_evaluation($restoreBackup, $requiredSince, true) : ['ready'=>false];
    $restoreReady = dental_release_candidate_timestamp_ready($restoreRaw, $requiredSince)
        && (string)($restoreEvidence['version'] ?? '') === $release
        && trim((string)($restoreEvidence['evidence_id'] ?? '')) !== ''
        && !empty($restoreBackupEvaluation['ready'])
        && trim((string)($restoreEvidence['backup_checksum_sha256'] ?? '')) !== ''
        && hash_equals((string)($restoreBackup['checksum_sha256'] ?? ''), (string)($restoreEvidence['backup_checksum_sha256'] ?? ''))
        && mb_strlen(trim((string)($restoreEvidence['note'] ?? ''))) >= 20;

    $manualEvidence = dental_release_candidate_manual_flow_evidence();
    $manualRaw = (string)($manualEvidence['tested_at'] ?? '');
    $manualTimestamp = $manualRaw !== '' ? (strtotime($manualRaw) ?: 0) : 0;
    $manualReady = dental_release_candidate_timestamp_ready($manualRaw, $requiredSince)
        && (string)($manualEvidence['version'] ?? '') === $release
        && trim((string)($manualEvidence['evidence_id'] ?? '')) !== ''
        && in_array((string)($manualEvidence['scope'] ?? ''), ['appointment','walk_in','both'], true)
        && mb_strlen(trim((string)($manualEvidence['note'] ?? ''))) >= 20;

    $smtp = dental_release_candidate_smtp_evidence($pdo, $requiredSince);
    $smtpReady = !empty($smtp['ready']);
    $emergencyFiles = dental_release_candidate_emergency_files();
    $canBuildInstallers = !function_exists('dental_distribution_feature_allowed') || dental_distribution_feature_allowed('package_builder');
    $packageSafety = !$canBuildInstallers;
    if ($canBuildInstallers) {
        try {
            require_once DENTAL_ROOT . '/app/Services/InstallerPackageService.php';
            $packageSafety = InstallerPackageService::releaseSafetyReady();
        } catch (Throwable $exception) {
            if (function_exists('dental_runtime_write_log')) dental_runtime_write_log('Release package safety check failed: ' . $exception->getMessage());
            $packageSafety = false;
        }
    }

    $gates = [
        dental_release_candidate_gate(
            'version',
            'Application and database version aligned',
            version_compare($installed, $release, '==') && !app_update_pending(),
            'The uploaded files and installed database marker must match with no pending or interrupted step.',
            $installed . ' / ' . $release,
            'system_setup.php?tab=updates',
            'Resume or apply update'
        ),
        dental_release_candidate_gate(
            'automated_validation',
            'Automated technical production validation',
            !empty($automated['ready']),
            'DentalCare automatically validates infrastructure, security, role routes, database safeguards, patient-ledger privacy, adaptive encounters, backup verification, interrupted-update recovery, and clean packaging.',
            (int)($automated['counts']['pass'] ?? 0) . ' passed; ' . (int)($automated['counts']['blocked'] ?? 0) . ' blocked',
            'production_monitoring.php#automated-validation',
            'View exact results'
        ),
        dental_release_candidate_gate(
            'smtp_delivery',
            'Clinic email delivery confirmed',
            $smtpReady,
            'Send one real test email through the configured clinic mail driver. DentalCare reads the successful delivery evidence automatically.',
            $smtpReady ? date('M j, Y g:i A', strtotime((string)$smtp['tested_at'])) : 'No current successful delivery',
            'email_settings.php',
            'Send test email'
        ),
        dental_release_candidate_gate(
            'manual_flow',
            'Clinic live workflow confirmed',
            $manualReady,
            'Confirm one real browser test through appointment or walk-in, chair, treatment, billing, payment, receipt, and checkout. Technical role and permission checks are automatic.',
            $manualReady ? date('M j, Y g:i A', $manualTimestamp) : 'Not confirmed for this release',
            'go_live_checklist.php#version-bound-flow',
            'Confirm clinic flow'
        ),
        dental_release_candidate_gate(
            'restore_test',
            'Backup restore confirmed',
            $restoreReady,
            'Record one staging, clean-server, or documented recovery drill linked to a deep-verified complete backup for this release.',
            $restoreReady ? date('M j, Y g:i A', $restoreTimestamp) : 'Not confirmed for this release',
            'backup_restore.php#restore-drill',
            'Verify backup and record restore'
        ),
        dental_release_candidate_gate(
            'emergency_files',
            'Emergency diagnostic entry points removed',
            $emergencyFiles === [],
            'Temporary diagnostic and one-time recovery PHP files must not remain accessible from the application root.',
            $emergencyFiles === [] ? 'Clear' : count($emergencyFiles) . ' found',
            'production_monitoring.php#release-candidate',
            'Remove files'
        ),
        dental_release_candidate_gate(
            'package_safety',
            $canBuildInstallers ? 'Clean installer safety rules pass' : 'Distributed-copy release factory disabled',
            $packageSafety,
            $canBuildInstallers
                ? 'The strict installer allowlist, sensitive-path exclusions, package manifest, and checksums must pass before distribution.'
                : 'This distributed installation is correctly locked from generating installers or issuing licenses.',
            $canBuildInstallers ? ($packageSafety ? 'Protected' : 'Needs review') : 'Distribution locked',
            $canBuildInstallers ? 'installer_builder.php' : '',
            $canBuildInstallers ? 'Review package safety' : ''
        ),
        dental_release_candidate_gate(
            'deployment_mode',
            'Deployment mode set to Production',
            dental_deployment_mode() === 'production',
            'Production packages are available only in Production deployment mode.',
            dental_deployment_mode_label(),
            'system_setup.php?tab=environment',
            'Set Production mode'
        ),
    ];

    $blockers = array_values(array_filter($gates, static fn(array $gate): bool => empty($gate['ready'])));
    $evidence = [
        'release' => $release,
        'installed' => $installed,
        'required_since' => $requiredSince,
        'automated_fingerprint' => (string)($automated['fingerprint'] ?? ''),
        'smtp' => $smtp,
        'manual_flow' => $manualEvidence,
        'restore_test' => $restoreEvidence,
        'restore_backup' => [
            'id' => (int)($restoreBackup['id'] ?? 0),
            'checksum_sha256' => (string)($restoreBackup['checksum_sha256'] ?? ''),
            'verified_at' => (string)($restoreBackup['verified_at'] ?? ''),
        ],
        'emergency_files' => $emergencyFiles,
        'package_safety' => $packageSafety,
        'deployment_mode' => dental_deployment_mode(),
    ];

    return [
        'release' => $release,
        'installed' => $installed,
        'required_since' => $requiredSince > 0 ? date('c', $requiredSince) : '',
        'ready' => $blockers === [],
        'gates' => $gates,
        'blockers' => $blockers,
        'roles' => ['rows'=>dental_production_role_prerequisites($pdo),'ready'=>empty(array_filter(dental_production_role_prerequisites($pdo), static fn(array $row): bool => empty($row['ready'])))],
        'production' => $automated,
        'automated_validation' => $automated,
        'smtp_evidence' => $smtp,
        'emergency_files' => $emergencyFiles,
        'backup_evidence' => dental_release_candidate_backup_evidence($pdo, $requiredSince),
        'evidence_sha256' => hash('sha256', json_encode($evidence, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: ''),
        'generated_at' => date('c'),
    ];
}

function dental_release_candidate_approval(): array
{
    $default = [
        'approval_id' => '',
        'version' => '',
        'approved_at' => '',
        'approved_by' => '',
        'approved_by_user_id' => 0,
        'note' => '',
        'evidence_sha256' => '',
    ];
    $raw = (string) setting_get('production_acceptance', 'release_candidate_approval', '');
    if ($raw === '') return $default;
    $decoded = json_decode($raw, true);
    return is_array($decoded) ? array_merge($default, $decoded) : $default;
}

function dental_release_candidate_package_matches_current_approval(array $packageApproval, array $currentApproval): bool
{
    $packageId = trim((string) ($packageApproval['approval_id'] ?? ''));
    $currentId = trim((string) ($currentApproval['approval_id'] ?? ''));
    $packageEvidence = trim((string) ($packageApproval['evidence_sha256'] ?? ''));
    $currentEvidence = trim((string) ($currentApproval['evidence_sha256'] ?? ''));

    return $packageId !== ''
        && $currentId !== ''
        && hash_equals($currentId, $packageId)
        && $packageEvidence !== ''
        && $currentEvidence !== ''
        && hash_equals($currentEvidence, $packageEvidence)
        && (string) ($packageApproval['version'] ?? '') === (string) ($currentApproval['version'] ?? '')
        && (string) ($packageApproval['approved_at'] ?? '') === (string) ($currentApproval['approved_at'] ?? '');
}

function dental_release_candidate_auto_approval(PDO $pdo, array $snapshot): array
{
    $approval = dental_release_candidate_approval();
    $currentEvidence = trim((string)($snapshot['evidence_sha256'] ?? ''));
    if ((string)($approval['version'] ?? '') === app_release_version()
        && trim((string)($approval['approval_id'] ?? '')) !== ''
        && trim((string)($approval['evidence_sha256'] ?? '')) !== ''
        && $currentEvidence !== ''
        && hash_equals((string)$approval['evidence_sha256'], $currentEvidence)) {
        return $approval;
    }

    $user = current_user() ?: [];
    $approval = [
        'approval_id' => bin2hex(random_bytes(16)),
        'version' => app_release_version(),
        'approved_at' => date('c'),
        'approved_by' => (string)($user['name'] ?? $user['email'] ?? 'DentalCare automated validation'),
        'approved_by_user_id' => (int)($user['id'] ?? 0),
        'note' => 'Automatically unlocked after all technical checks and the three clinic-specific live confirmations passed.',
        'evidence_sha256' => $currentEvidence,
    ];
    setting_set('production_acceptance', 'release_candidate_approval', json_encode($approval, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
    setting_set('production_acceptance', 'release_candidate_status', 'approved');
    setting_set('production_acceptance', 'release_candidate_version', app_release_version());
    return $approval;
}

function dental_release_candidate_is_approved(PDO $pdo): bool
{
    if ((string)setting_get('production_acceptance', 'release_candidate_manual_hold', '0') === '1') return false;
    $snapshot = dental_release_candidate_snapshot($pdo);
    if (empty($snapshot['ready'])) return false;
    $approval = dental_release_candidate_auto_approval($pdo, $snapshot);
    return trim((string)($approval['approval_id'] ?? '')) !== ''
        && (string)($approval['version'] ?? '') === app_release_version()
        && trim((string)($approval['evidence_sha256'] ?? '')) !== ''
        && hash_equals((string)$approval['evidence_sha256'], (string)($snapshot['evidence_sha256'] ?? ''));
}

function dental_release_candidate_record_approval(PDO $pdo, string $confirmation, string $note): array
{
    $snapshot = dental_release_candidate_snapshot($pdo);
    if (empty($snapshot['ready'])) {
        $labels = array_map(static fn(array $gate): string => (string)$gate['title'], (array)$snapshot['blockers']);
        throw new RuntimeException('Production validation is still blocked: ' . implode('; ', $labels) . '.');
    }
    setting_set('production_acceptance', 'release_candidate_manual_hold', '0');
    return dental_release_candidate_auto_approval($pdo, $snapshot);
}

function dental_release_candidate_revoke(): void
{
    setting_set('production_acceptance', 'release_candidate_approval', '');
    setting_set('production_acceptance', 'release_candidate_status', 'held');
    setting_set('production_acceptance', 'release_candidate_manual_hold', '1');
}

function dental_release_candidate_public_approval(PDO $pdo): array
{
    if (!dental_release_candidate_is_approved($pdo)) return [];
    $approval = dental_release_candidate_approval();
    return [
        'approval_id' => (string) ($approval['approval_id'] ?? ''),
        'version' => (string) ($approval['version'] ?? ''),
        'approved_at' => (string) ($approval['approved_at'] ?? ''),
        'evidence_sha256' => (string) ($approval['evidence_sha256'] ?? ''),
    ];
}
