<?php

declare(strict_types=1);

/**
 * Deterministic, resumable migration runner.
 *
 * From v0.9 onward, execution is controlled by immutable migration keys and
 * SHA-256 checksums stored in system_migration_ledger. Readiness callbacks are
 * postconditions and interruption-recovery probes; they do not cause completed
 * historical migrations to replay.
 */

function dental_update_checkpoint_table(): string
{
    return table('system_update_runs');
}

function dental_migration_ledger_table(): string
{
    return table('system_migration_ledger');
}

function dental_update_schema_column_exists(PDO $pdo, string $tableName, string $column): bool
{
    $stmt = $pdo->prepare('SELECT 1 FROM information_schema.columns WHERE table_schema=? AND table_name=? AND column_name=? LIMIT 1');
    $stmt->execute([(string)app_config('database.name'), $tableName, $column]);
    return (bool)$stmt->fetchColumn();
}

function dental_update_checkpoint_ensure_schema(PDO $pdo): void
{
    $table = dental_update_checkpoint_table();
    $pdo->exec('CREATE TABLE IF NOT EXISTS `' . $table . '` (
        `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        `run_id` CHAR(32) NOT NULL,
        `target_version` VARCHAR(40) NOT NULL,
        `status` VARCHAR(30) NOT NULL DEFAULT "pending",
        `plan_json` LONGTEXT NOT NULL,
        `completed_json` LONGTEXT NULL,
        `current_version` VARCHAR(40) NULL,
        `current_label` VARCHAR(190) NULL,
        `step_index` INT UNSIGNED NOT NULL DEFAULT 0,
        `step_count` INT UNSIGNED NOT NULL DEFAULT 0,
        `last_error` TEXT NULL,
        `error_reference` VARCHAR(40) NULL,
        `started_by` BIGINT UNSIGNED NULL,
        `started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `updated_at` DATETIME NULL,
        `completed_at` DATETIME NULL,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uq_system_update_run_id` (`run_id`),
        KEY `idx_system_update_status` (`status`,`target_version`,`updated_at`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci');
    dental_migration_ledger_ensure_schema($pdo);
}

function dental_migration_ledger_ensure_schema(PDO $pdo): void
{
    $table = dental_migration_ledger_table();
    $pdo->exec('CREATE TABLE IF NOT EXISTS `' . $table . '` (
        `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        `version` VARCHAR(40) NOT NULL,
        `package_version` VARCHAR(40) NULL,
        `migration_key` VARCHAR(120) NOT NULL,
        `checksum_sha256` CHAR(64) NULL,
        `status` VARCHAR(30) NOT NULL DEFAULT "pending",
        `attempt_count` INT UNSIGNED NOT NULL DEFAULT 0,
        `applied_by` BIGINT UNSIGNED NULL,
        `started_at` DATETIME NULL,
        `applied_at` DATETIME NULL,
        `completed_at` DATETIME NULL,
        `updated_at` DATETIME NULL,
        `execution_ms` INT UNSIGNED NULL,
        `error_reference` VARCHAR(40) NULL,
        `error_message` TEXT NULL,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uq_system_migration_key` (`migration_key`),
        KEY `idx_system_migration_version` (`version`,`status`,`completed_at`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci');

    $columns = [
        'package_version' => 'VARCHAR(40) NULL',
        'attempt_count' => 'INT UNSIGNED NOT NULL DEFAULT 0',
        'started_at' => 'DATETIME NULL',
        'completed_at' => 'DATETIME NULL',
        'updated_at' => 'DATETIME NULL',
        'execution_ms' => 'INT UNSIGNED NULL',
    ];
    foreach ($columns as $column => $definition) {
        if (!dental_update_schema_column_exists($pdo, $table, $column)) {
            $pdo->exec('ALTER TABLE `' . $table . '` ADD COLUMN `' . $column . '` ' . $definition);
        }
    }
    // Existing v0.8 rows used applied_at as the terminal timestamp.
    $pdo->exec('UPDATE `' . $table . '` SET completed_at=COALESCE(completed_at,applied_at), updated_at=COALESCE(updated_at,applied_at), status="completed" WHERE status="completed" AND completed_at IS NULL');
}

function dental_update_checkpoint_normalize_plan(array $plan): array
{
    $normalized = [];
    foreach ($plan as $step) {
        if (!is_array($step)) continue;
        $version = trim((string)($step['version'] ?? ''));
        $apply = trim((string)($step['apply'] ?? ''));
        $ready = trim((string)($step['ready'] ?? ''));
        $migrationKey = trim((string)($step['migration_key'] ?? $version));
        $migrationFile = trim((string)($step['migration_file'] ?? ''));
        $checksum = strtolower(trim((string)($step['checksum_sha256'] ?? '')));
        if ($checksum === '' && $migrationFile !== '' && is_file($migrationFile)) {
            $checksum = strtolower((string)hash_file('sha256', $migrationFile));
        }
        if ($version === '' || $migrationKey === '' || $apply === '' || $ready === '') continue;
        if (!is_callable($apply) || !is_callable($ready)) {
            throw new RuntimeException('Update package is incomplete for migration ' . $migrationKey . '.');
        }
        if ($checksum !== '' && !preg_match('/^[a-f0-9]{64}$/', $checksum)) {
            throw new RuntimeException('Update package contains an invalid checksum for migration ' . $migrationKey . '.');
        }
        $normalized[] = [
            'version' => $version,
            'migration_key' => $migrationKey,
            'migration_file' => $migrationFile,
            'checksum_sha256' => $checksum,
            'label' => trim((string)($step['label'] ?? ('Apply migration ' . $migrationKey))),
            'apply' => $apply,
            'ready' => $ready,
        ];
    }
    usort($normalized, static function(array $a, array $b): int {
        $versionCompare = version_compare((string)$a['version'], (string)$b['version']);
        return $versionCompare !== 0 ? $versionCompare : strcmp((string)$a['migration_key'], (string)$b['migration_key']);
    });
    return $normalized;
}

function dental_migration_ledger_row(PDO $pdo, string $migrationKey): ?array
{
    dental_migration_ledger_ensure_schema($pdo);
    $stmt = $pdo->prepare('SELECT * FROM `' . dental_migration_ledger_table() . '` WHERE migration_key=? LIMIT 1');
    $stmt->execute([$migrationKey]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function dental_migration_checksum_is_officially_compatible(string $migrationKey, string $storedChecksum, string $packageChecksum): bool
{
    $storedChecksum = strtolower(trim($storedChecksum));
    $packageChecksum = strtolower(trim($packageChecksum));
    if ($storedChecksum === '' || $packageChecksum === '') return false;
    if (hash_equals($storedChecksum, $packageChecksum)) return true;

    // v0.9.1.2 had one official upgrade-safe repair package after the initial
    // release. Both exact SHA-256 values are trusted for this migration only.
    // Unknown changes continue to fail closed. Historical migrations must not
    // be edited again; future repairs require a new migration key.
    $officialAliases = [
        '0.9.1.2_patient_portal_controlled_record_release' => [
            '8001a2f35a730f7cbd622d9839e1e6cdc2f16fa61a77b71ee9c34917eb301fd2',
            'ea5a80c731c4ac8793a29c546ded867373ba3a9010f29147b1527137b7eb9ad2',
        ],
    ];
    $allowed = $officialAliases[$migrationKey] ?? [];
    return in_array($storedChecksum, $allowed, true)
        && in_array($packageChecksum, $allowed, true);
}

function dental_migration_ledger_completed(PDO $pdo, string $migrationKey, string $checksum = ''): bool
{
    $row = dental_migration_ledger_row($pdo, $migrationKey);
    if (!$row || strtolower((string)$row['status']) !== 'completed') return false;
    $stored = strtolower(trim((string)($row['checksum_sha256'] ?? '')));
    if ($checksum !== '' && $stored !== ''
        && !dental_migration_checksum_is_officially_compatible($migrationKey, $stored, strtolower($checksum))) {
        throw new RuntimeException('Completed migration checksum mismatch for ' . $migrationKey . '. Restore the original release package before continuing.');
    }
    return true;
}

function dental_migration_ledger_mark_running(PDO $pdo, array $step): void
{
    dental_migration_ledger_ensure_schema($pdo);
    $stmt = $pdo->prepare('INSERT INTO `' . dental_migration_ledger_table() . '` (version,package_version,migration_key,checksum_sha256,status,attempt_count,applied_by,started_at,updated_at,error_reference,error_message) VALUES (?,?,?,?,"running",1,?,NOW(),NOW(),NULL,NULL) '
        . 'ON DUPLICATE KEY UPDATE version=VALUES(version),package_version=VALUES(package_version),checksum_sha256=VALUES(checksum_sha256),status="running",attempt_count=attempt_count+1,applied_by=VALUES(applied_by),started_at=NOW(),updated_at=NOW(),error_reference=NULL,error_message=NULL');
    $stmt->execute([
        (string)$step['version'],
        app_release_version(),
        (string)$step['migration_key'],
        (string)$step['checksum_sha256'] !== '' ? (string)$step['checksum_sha256'] : null,
        (int)(current_user()['id'] ?? 0) ?: null,
    ]);
}

function dental_migration_ledger_mark_completed(PDO $pdo, array $step, int $executionMs, bool $recovered = false): void
{
    dental_migration_ledger_ensure_schema($pdo);
    $stmt = $pdo->prepare('INSERT INTO `' . dental_migration_ledger_table() . '` (version,package_version,migration_key,checksum_sha256,status,attempt_count,applied_by,started_at,applied_at,completed_at,updated_at,execution_ms,error_reference,error_message) VALUES (?,?,?,?,"completed",1,?,NOW(),NOW(),NOW(),NOW(),?,NULL,NULL) '
        . 'ON DUPLICATE KEY UPDATE version=VALUES(version),package_version=VALUES(package_version),checksum_sha256=VALUES(checksum_sha256),status="completed",applied_by=VALUES(applied_by),applied_at=NOW(),completed_at=NOW(),updated_at=NOW(),execution_ms=VALUES(execution_ms),error_reference=NULL,error_message=NULL');
    $stmt->execute([
        (string)$step['version'],
        app_release_version(),
        (string)$step['migration_key'],
        (string)$step['checksum_sha256'] !== '' ? (string)$step['checksum_sha256'] : null,
        (int)(current_user()['id'] ?? 0) ?: null,
        max(0, $executionMs),
    ]);
    if ($recovered && function_exists('audit_required')) {
        audit_required('system_migration_recovered_after_interruption', 'system', null, [
            'migration_key' => (string)$step['migration_key'],
            'version' => (string)$step['version'],
        ]);
    }
}

function dental_migration_ledger_mark_failed(PDO $pdo, array $step, string $reference, string $message, int $executionMs): void
{
    dental_migration_ledger_ensure_schema($pdo);
    $stmt = $pdo->prepare('INSERT INTO `' . dental_migration_ledger_table() . '` (version,package_version,migration_key,checksum_sha256,status,attempt_count,applied_by,started_at,updated_at,execution_ms,error_reference,error_message) VALUES (?,?,?,?,"failed",1,?,NOW(),NOW(),?,?,?) '
        . 'ON DUPLICATE KEY UPDATE version=VALUES(version),package_version=VALUES(package_version),checksum_sha256=VALUES(checksum_sha256),status="failed",applied_by=VALUES(applied_by),updated_at=NOW(),execution_ms=VALUES(execution_ms),error_reference=VALUES(error_reference),error_message=VALUES(error_message)');
    $stmt->execute([
        (string)$step['version'],
        app_release_version(),
        (string)$step['migration_key'],
        (string)$step['checksum_sha256'] !== '' ? (string)$step['checksum_sha256'] : null,
        (int)(current_user()['id'] ?? 0) ?: null,
        max(0, $executionMs),
        $reference,
        mb_substr($message, 0, 4000),
    ]);
}

function dental_update_checkpoint_active(PDO $pdo, string $targetVersion): ?array
{
    dental_update_checkpoint_ensure_schema($pdo);
    $stmt = $pdo->prepare('SELECT * FROM `' . dental_update_checkpoint_table() . '` WHERE target_version=? AND status IN ("pending","running","failed") ORDER BY id DESC LIMIT 1');
    $stmt->execute([$targetVersion]);
    $row = $stmt->fetch();
    if (!$row) return null;

    // A request can finish the final migration and lose its connection before
    // the checkpoint row is changed from pending to completed. Normalize that
    // stale terminal row from the exact migration ledger before offering Resume.
    $row = dental_update_checkpoint_normalize_terminal_row($pdo, $row);
    return strtolower((string)($row['status'] ?? '')) === 'completed' ? null : $row;
}

function dental_update_checkpoint_latest(PDO $pdo): ?array
{
    try {
        dental_update_checkpoint_ensure_schema($pdo);
        $row = $pdo->query('SELECT * FROM `' . dental_update_checkpoint_table() . '` ORDER BY id DESC LIMIT 1')->fetch();
        if (!$row) return null;
        return dental_update_checkpoint_normalize_terminal_row($pdo, $row);
    } catch (Throwable $exception) {
        error_log('DentalCare update checkpoint status lookup deferred: ' . $exception->getMessage());
        return null;
    }
}

function dental_update_checkpoint_plan_for_run(array $row, array $fallbackPlan): array
{
    $decoded = json_decode((string)($row['plan_json'] ?? ''), true);
    if (!is_array($decoded) || $decoded === []) return $fallbackPlan;
    $byKey = [];
    foreach ($fallbackPlan as $step) $byKey[(string)$step['migration_key']] = $step;
    $result = [];
    foreach ($decoded as $stored) {
        $key = trim((string)($stored['migration_key'] ?? $stored['version'] ?? ''));
        if ($key !== '' && isset($byKey[$key])) $result[] = $byKey[$key];
    }
    return $result ?: $fallbackPlan;
}

function dental_update_checkpoint_begin(PDO $pdo, array $plan, string $targetVersion): array
{
    dental_update_checkpoint_ensure_schema($pdo);
    $existing = dental_update_checkpoint_active($pdo, $targetVersion);
    if ($existing) return $existing;

    $serialized = array_map(static fn(array $step): array => [
        'version' => $step['version'],
        'migration_key' => $step['migration_key'],
        'checksum_sha256' => $step['checksum_sha256'],
        'label' => $step['label'],
    ], $plan);
    $runId = bin2hex(random_bytes(16));
    $stmt = $pdo->prepare('INSERT INTO `' . dental_update_checkpoint_table() . '` (run_id,target_version,status,plan_json,completed_json,step_index,step_count,started_by,started_at,updated_at) VALUES (?,? ,"pending",?,"[]",0,?,?,NOW(),NOW())');
    $stmt->execute([
        $runId,
        $targetVersion,
        json_encode($serialized, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        count($plan),
        (int)(current_user()['id'] ?? 0) ?: null,
    ]);
    return dental_update_checkpoint_active($pdo, $targetVersion) ?: [];
}

function dental_update_checkpoint_public(array $row): array
{
    $status = (string)($row['status'] ?? 'pending');
    $stepCount = max(0, (int)($row['step_count'] ?? 0));
    $stepIndex = max(0, (int)($row['step_index'] ?? 0));
    $percent = $stepCount > 0 ? (int)floor(min(1, $stepIndex / $stepCount) * 100) : ($status === 'completed' ? 100 : 0);
    return [
        'run_id' => (string)($row['run_id'] ?? ''),
        'target_version' => (string)($row['target_version'] ?? ''),
        'status' => $status,
        'step_index' => $stepIndex,
        'step_count' => $stepCount,
        'percent' => $status === 'completed' ? 100 : $percent,
        'current_version' => (string)($row['current_version'] ?? ''),
        'current_label' => (string)($row['current_label'] ?? ''),
        'last_error' => (string)($row['last_error'] ?? ''),
        'error_reference' => (string)($row['error_reference'] ?? ''),
        'updated_at' => (string)($row['updated_at'] ?? ''),
        'completed_at' => (string)($row['completed_at'] ?? ''),
    ];
}

/**
 * Return the immutable migration descriptors stored when this update run began.
 * The checkpoint plan, not completed_json, is the boundary for progress counts.
 */
function dental_update_checkpoint_stored_plan(array $row): array
{
    $storedPlan = json_decode((string)($row['plan_json'] ?? ''), true);
    if (!is_array($storedPlan)) return [];

    $result = [];
    $seen = [];
    foreach ($storedPlan as $storedStep) {
        if (!is_array($storedStep)) continue;
        $key = trim((string)($storedStep['migration_key'] ?? $storedStep['version'] ?? ''));
        if ($key === '' || isset($seen[$key])) continue;
        $seen[$key] = true;
        $result[] = [
            'version' => trim((string)($storedStep['version'] ?? '')),
            'migration_key' => $key,
            'checksum_sha256' => strtolower(trim((string)($storedStep['checksum_sha256'] ?? ''))),
            'label' => trim((string)($storedStep['label'] ?? '')),
        ];
    }
    return $result;
}

/**
 * Derive progress from the authoritative migration ledger. Historical or
 * duplicated keys in completed_json must never inflate the displayed count.
 */
function dental_update_checkpoint_completed_keys(PDO $pdo, array $row): array
{
    $completed = [];
    foreach (dental_update_checkpoint_stored_plan($row) as $storedStep) {
        $key = (string)$storedStep['migration_key'];
        try {
            if (dental_migration_ledger_completed($pdo, $key, (string)$storedStep['checksum_sha256'])) {
                $completed[] = $key;
            }
        } catch (Throwable) {
            // A checksum or ledger error is handled by the normal update path.
            // Do not count the step as complete merely because completed_json says so.
        }
    }
    return array_values(array_unique($completed));
}

function dental_update_checkpoint_step_position(array $row, string $migrationKey, array $fallbackPlan = []): int
{
    $storedPlan = dental_update_checkpoint_stored_plan($row);
    foreach ($storedPlan as $index => $storedStep) {
        if ((string)$storedStep['migration_key'] === $migrationKey) return $index + 1;
    }
    foreach ($fallbackPlan as $index => $step) {
        if ((string)($step['migration_key'] ?? '') === $migrationKey) return $index + 1;
    }
    return 1;
}

/**
 * Repair impossible progress such as "Completed 2 of 2" when only the first
 * migration is present in the ledger. This performs no migration work.
 */
function dental_update_checkpoint_normalize_progress_row(PDO $pdo, array $row): array
{
    if (strtolower((string)($row['status'] ?? '')) === 'completed') return $row;

    $storedPlan = dental_update_checkpoint_stored_plan($row);
    if ($storedPlan === []) return $row;

    $completedKeys = dental_update_checkpoint_completed_keys($pdo, $row);
    $stepCount = count($storedPlan);
    $stepIndex = min($stepCount, count($completedKeys));
    $storedCompleted = json_decode((string)($row['completed_json'] ?? '[]'), true);
    if (!is_array($storedCompleted)) $storedCompleted = [];
    $storedCompleted = array_values(array_unique(array_map('strval', $storedCompleted)));

    $needsRepair = (int)($row['step_count'] ?? 0) !== $stepCount
        || (int)($row['step_index'] ?? 0) !== $stepIndex
        || $storedCompleted !== $completedKeys;
    if (!$needsRepair) return $row;

    $label = (string)($row['current_label'] ?? '');
    if (strtolower((string)($row['status'] ?? '')) !== 'failed' && $stepIndex < $stepCount) {
        $label = 'Ready to resume remaining migration';
    }

    $stmt = $pdo->prepare(
        'UPDATE `' . dental_update_checkpoint_table() . '` '
        . 'SET completed_json=?,step_index=?,step_count=?,current_label=?,updated_at=NOW() WHERE id=?'
    );
    $stmt->execute([
        json_encode($completedKeys, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        $stepIndex,
        $stepCount,
        $label,
        (int)($row['id'] ?? 0),
    ]);

    $fresh = $pdo->query('SELECT * FROM `' . dental_update_checkpoint_table() . '` WHERE id=' . (int)($row['id'] ?? 0) . ' LIMIT 1')->fetch();
    return $fresh ?: $row;
}

/**
 * Confirm that every immutable migration stored in a checkpoint plan is already
 * completed in the authoritative ledger and that the installed version reached
 * the run target. No readiness callback or migration body is executed here.
 */
function dental_update_checkpoint_terminal_ready(PDO $pdo, array $row): bool
{
    if (strtolower((string)($row['status'] ?? '')) === 'completed') return true;

    $targetVersion = trim((string)($row['target_version'] ?? ''));
    if ($targetVersion === '' || version_compare(installed_application_version(), $targetVersion, '<')) {
        return false;
    }

    $storedPlan = json_decode((string)($row['plan_json'] ?? ''), true);
    if (!is_array($storedPlan) || $storedPlan === []) return false;

    $keys = [];
    foreach ($storedPlan as $storedStep) {
        if (!is_array($storedStep)) return false;
        $key = trim((string)($storedStep['migration_key'] ?? $storedStep['version'] ?? ''));
        $checksum = strtolower(trim((string)($storedStep['checksum_sha256'] ?? '')));
        if ($key === '' || !dental_migration_ledger_completed($pdo, $key, $checksum)) return false;
        $keys[] = $key;
    }

    $expectedCount = max(0, (int)($row['step_count'] ?? 0));
    return $keys !== [] && ($expectedCount === 0 || count(array_unique($keys)) >= $expectedCount);
}

function dental_update_checkpoint_normalize_terminal_row(PDO $pdo, array $row): array
{
    if (strtolower((string)($row['status'] ?? '')) === 'completed') return $row;

    // First correct the progress counter from the immutable plan and ledger.
    // This prevents stale completed_json values from displaying 2/2 while a
    // later package migration is still genuinely pending.
    $row = dental_update_checkpoint_normalize_progress_row($pdo, $row);
    if (!dental_update_checkpoint_terminal_ready($pdo, $row)) return $row;

    $completedKeys = dental_update_checkpoint_completed_keys($pdo, $row);
    $stmt = $pdo->prepare(
        'UPDATE `' . dental_update_checkpoint_table() . '` '
        . 'SET status="completed",completed_json=?,step_index=step_count,current_version=?,current_label="Migration verification complete",last_error=NULL,error_reference=NULL,updated_at=NOW(),completed_at=COALESCE(completed_at,NOW()) '
        . 'WHERE id=? AND status IN ("pending","running","failed")'
    );
    $stmt->execute([
        json_encode($completedKeys, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        (string)($row['target_version'] ?? ''),
        (int)($row['id'] ?? 0),
    ]);

    $fresh = $pdo->query('SELECT * FROM `' . dental_update_checkpoint_table() . '` WHERE id=' . (int)($row['id'] ?? 0) . ' LIMIT 1')->fetch();
    return $fresh ?: $row;
}

function dental_update_checkpoint_fail_result(PDO $pdo, array $row, array $step, int $position, int $stepCount, Throwable $exception, int $executionMs = 0): array
{
    $reference = strtoupper(substr(hash('sha256', microtime(true) . '|' . random_bytes(16)), 0, 12));
    $message = mb_substr($exception->getMessage(), 0, 2000);
    dental_migration_ledger_mark_failed($pdo, $step, $reference, $message, $executionMs);
    $stmt = $pdo->prepare('UPDATE `' . dental_update_checkpoint_table() . '` SET status="failed",last_error=?,error_reference=?,updated_at=NOW() WHERE id=?');
    $stmt->execute([$message, $reference, (int)$row['id']]);
    if (function_exists('dental_runtime_write_log')) {
        dental_runtime_write_log('Migration failed [' . $reference . '] ' . $step['migration_key'] . ': ' . $exception->getMessage());
    } else {
        error_log('DentalCare migration failed [' . $reference . ']: ' . $exception->getMessage());
    }
    return [
        'status' => 'failed',
        'message' => 'Migration ' . $step['migration_key'] . ' failed. Correct the reported problem, then click Retry failed migration.',
        'last_error' => $message,
        'error_reference' => $reference,
        'step_index' => max(0, $position - 1),
        'step_count' => $stepCount,
        'percent' => $stepCount > 0 ? (int)floor((max(0, $position - 1) / $stepCount) * 100) : 0,
        'current_version' => (string)$step['version'],
        'current_label' => (string)$step['label'],
    ];
}


/**
 * Reconcile migrations whose database/runtime postconditions are already true.
 *
 * This is deliberately verification-only: it never calls a migration apply
 * function and therefore is safe to run while rendering the update-status page.
 * It repairs the exact failure mode where an idempotent migration completed its
 * writes but the request stopped before the deterministic ledger/checkpoint was
 * marked completed.
 */
function dental_update_checkpoint_reconcile_ready(PDO $pdo, array $rawPlan, string $targetVersion): array
{
    $plan = dental_update_checkpoint_normalize_plan($rawPlan);
    $completedKeys = [];
    $reconciledKeys = [];
    $pendingKeys = [];
    $errors = [];

    foreach ($plan as $step) {
        $key = (string)$step['migration_key'];
        try {
            if (dental_migration_ledger_completed($pdo, $key, (string)$step['checksum_sha256'])) {
                $completedKeys[] = $key;
                continue;
            }

            if (function_exists('setting_request_cache_clear')) setting_request_cache_clear();
            if (!(bool)call_user_func($step['ready'], $pdo)) {
                $pendingKeys[] = $key;
                continue;
            }

            // The state is already applied. Only repair the authoritative ledger;
            // do not execute the migration again.
            dental_migration_ledger_mark_completed($pdo, $step, 0, false);
            $completedKeys[] = $key;
            $reconciledKeys[] = $key;
            try {
                if (function_exists('audit')) {
                    audit('system_migration_status_reconciled', 'system', null, [
                        'migration_key' => $key,
                        'version' => (string)$step['version'],
                        'reason' => 'postconditions_already_satisfied',
                    ]);
                }
            } catch (Throwable) {
                // Ledger reconciliation must not be reversed by optional audit I/O.
            }
        } catch (Throwable $exception) {
            $pendingKeys[] = $key;
            $errors[$key] = mb_substr($exception->getMessage(), 0, 1000);
        }
    }

    $allReady = $pendingKeys === []
        && version_compare(installed_application_version(), $targetVersion, '>=');

    if ($allReady) {
        try {
            dental_update_checkpoint_ensure_schema($pdo);
            $allKeys = array_values(array_unique(array_merge(
                $completedKeys,
                array_map(static fn(array $step): string => (string)$step['migration_key'], $plan)
            )));
            $stmt = $pdo->prepare(
                'UPDATE `' . dental_update_checkpoint_table() . '` '
                . 'SET status="completed",completed_json=?,step_index=step_count,current_version=?,current_label="Migration verification complete",last_error=NULL,error_reference=NULL,updated_at=NOW(),completed_at=COALESCE(completed_at,NOW()) '
                . 'WHERE target_version=? AND status IN ("pending","running","failed")'
            );
            $stmt->execute([
                json_encode($allKeys, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                $targetVersion,
                $targetVersion,
            ]);
        } catch (Throwable $exception) {
            $errors['checkpoint'] = mb_substr($exception->getMessage(), 0, 1000);
            $allReady = false;
        }
    }

    unset($_SESSION['dental_update_pending_cache']);
    if (function_exists('setting_request_cache_clear')) setting_request_cache_clear();

    return [
        'ready' => $allReady,
        'reconciled' => $reconciledKeys,
        'pending' => array_values(array_unique($pendingKeys)),
        'errors' => $errors,
    ];
}

function dental_update_checkpoint_run_next(PDO $pdo, array $rawPlan, string $targetVersion): array
{
    $plan = dental_update_checkpoint_normalize_plan($rawPlan);
    $lockName = 'dental_update_' . substr(hash('sha256', (string)app_config('database.name') . '|' . (string)app_config('database.prefix', 'dent_')), 0, 40);
    $lockStmt = $pdo->prepare('SELECT GET_LOCK(?,0)');
    $lockStmt->execute([$lockName]);
    if ((int)$lockStmt->fetchColumn() !== 1) {
        return ['status' => 'busy', 'message' => 'Another migration request is running. Wait for it to finish, then retry.', 'percent' => 0];
    }

    try {
        $row = dental_update_checkpoint_active($pdo, $targetVersion);
        if (!$row) $row = dental_update_checkpoint_begin($pdo, $plan, $targetVersion);
        if (!$row) throw new RuntimeException('Unable to create the update recovery record.');

        $row = dental_update_checkpoint_normalize_progress_row($pdo, $row);
        $storedPlan = dental_update_checkpoint_stored_plan($row);
        $storedStepCount = $storedPlan !== [] ? count($storedPlan) : max(1, count($plan));
        $plan = dental_update_checkpoint_plan_for_run($row, $plan);
        $completedKeys = dental_update_checkpoint_completed_keys($pdo, $row);

        $next = null;
        foreach ($plan as $step) {
            $key = (string)$step['migration_key'];
            try {
                if (dental_migration_ledger_completed($pdo, $key, (string)$step['checksum_sha256'])) {
                    if (!in_array($key, $completedKeys, true)) $completedKeys[] = $key;
                    continue;
                }
                // Recovery path: the request may have been interrupted after the
                // idempotent database work but before the ledger was completed.
                if (function_exists('setting_request_cache_clear')) setting_request_cache_clear();
                if ((bool)call_user_func($step['ready'], $pdo)) {
                    dental_migration_ledger_mark_completed($pdo, $step, 0, true);
                    if (!in_array($key, $completedKeys, true)) $completedKeys[] = $key;
                    continue;
                }
            } catch (Throwable $exception) {
                $position = dental_update_checkpoint_step_position($row, $key, $plan);
                return dental_update_checkpoint_fail_result($pdo, $row, $step, $position, $storedStepCount, $exception);
            }
            $next = $step;
            break;
        }

        if ($next === null) {
            if (version_compare(installed_application_version(), $targetVersion, '<')) {
                throw new RuntimeException('The checkpoint contains no executable remaining migration, but the database version marker is still below ' . $targetVersion . '. Refresh the uploaded package files, then retry.');
            }
            if (!dental_update_checkpoint_terminal_ready($pdo, $row)) {
                throw new RuntimeException('The database version reached ' . $targetVersion . ', but one or more immutable migration ledger entries are incomplete.');
            }
            $completedKeys = dental_update_checkpoint_completed_keys($pdo, $row);
            $stmt = $pdo->prepare('UPDATE `' . dental_update_checkpoint_table() . '` SET status="completed",completed_json=?,step_index=step_count,current_version=?,current_label="Migration verification complete",last_error=NULL,error_reference=NULL,updated_at=NOW(),completed_at=NOW() WHERE id=?');
            $stmt->execute([json_encode($completedKeys, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), $targetVersion, (int)$row['id']]);
            unset($_SESSION['dental_update_pending_cache']);
            $fresh = $pdo->query('SELECT * FROM `' . dental_update_checkpoint_table() . '` WHERE id=' . (int)$row['id'])->fetch() ?: $row;
            $public = dental_update_checkpoint_public($fresh);
            $public['message'] = 'Update completed. Every package migration is recorded in the deterministic ledger.';
            return $public;
        }

        $position = dental_update_checkpoint_step_position($row, (string)$next['migration_key'], $plan);
        $completedIndex = min($storedStepCount, count($completedKeys));
        $stmt = $pdo->prepare('UPDATE `' . dental_update_checkpoint_table() . '` SET status="running",current_version=?,current_label=?,step_index=?,step_count=?,completed_json=?,last_error=NULL,error_reference=NULL,updated_at=NOW() WHERE id=?');
        $stmt->execute([
            (string)$next['version'],
            (string)$next['label'],
            $completedIndex,
            $storedStepCount,
            json_encode($completedKeys, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            (int)$row['id'],
        ]);

        $started = microtime(true);
        try {
            dental_migration_ledger_mark_running($pdo, $next);
            call_user_func($next['apply'], $pdo);
            if (function_exists('setting_request_cache_clear')) setting_request_cache_clear();
            if (!(bool)call_user_func($next['ready'], $pdo)) {
                throw new RuntimeException('Migration ' . $next['migration_key'] . ' did not satisfy its postconditions.');
            }
            $executionMs = (int)round((microtime(true) - $started) * 1000);
            dental_migration_ledger_mark_completed($pdo, $next, $executionMs);

            // Rebuild completion from the ledger after the write. Never append to
            // an untrusted completed_json list from an older interrupted request.
            $completedKeys = dental_update_checkpoint_completed_keys($pdo, $row);
            if (!in_array((string)$next['migration_key'], $completedKeys, true)) {
                $completedKeys[] = (string)$next['migration_key'];
            }
            $completedKeys = array_values(array_unique($completedKeys));

            if (function_exists('setting_request_cache_clear')) setting_request_cache_clear();
            if (dental_update_checkpoint_terminal_ready($pdo, $row)) {
                $stmt = $pdo->prepare('UPDATE `' . dental_update_checkpoint_table() . '` SET status="completed",completed_json=?,step_index=step_count,current_version=?,current_label="Migration verification complete",last_error=NULL,error_reference=NULL,updated_at=NOW(),completed_at=NOW() WHERE id=?');
                $stmt->execute([
                    json_encode($completedKeys, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                    $targetVersion,
                    (int)$row['id'],
                ]);
                unset($_SESSION['dental_update_pending_cache']);
                $fresh = $pdo->query('SELECT * FROM `' . dental_update_checkpoint_table() . '` WHERE id=' . (int)$row['id'])->fetch() ?: $row;
                $public = dental_update_checkpoint_public($fresh);
                $public['message'] = 'Update completed. Every package migration is recorded in the deterministic ledger.';
                return $public;
            }

            $stepIndex = min($storedStepCount, count($completedKeys));
            $stmt = $pdo->prepare('UPDATE `' . dental_update_checkpoint_table() . '` SET status="pending",completed_json=?,step_index=?,step_count=?,current_version=?,current_label=?,last_error=NULL,error_reference=NULL,updated_at=NOW() WHERE id=?');
            $stmt->execute([
                json_encode($completedKeys, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
                $stepIndex,
                $storedStepCount,
                (string)$next['version'],
                'Completed ' . (string)$next['label'],
                (int)$row['id'],
            ]);
        } catch (Throwable $exception) {
            $executionMs = (int)round((microtime(true) - $started) * 1000);
            return dental_update_checkpoint_fail_result($pdo, $row, $next, $position, $storedStepCount, $exception, $executionMs);
        }

        $fresh = $pdo->query('SELECT * FROM `' . dental_update_checkpoint_table() . '` WHERE id=' . (int)$row['id'])->fetch() ?: $row;
        $fresh = dental_update_checkpoint_normalize_terminal_row($pdo, $fresh);
        $public = dental_update_checkpoint_public($fresh);
        $public['message'] = 'Completed migration ' . (string)$next['migration_key'] . '. Continuing with the next package migration.';
        return $public;
    } finally {
        try {
            $releaseStmt = $pdo->prepare('SELECT RELEASE_LOCK(?)');
            $releaseStmt->execute([$lockName]);
        } catch (Throwable) {
        }
    }
}
