import { createHash } from 'node:crypto';

import type { Pool as MariaPool } from 'mariadb';
import type { Pool as PgPool, PoolClient } from 'pg';

import {
    isLegacyArchiveProfile,
    normalizeArchivedGeneral,
    type ArchivedGeneralSourceFormat,
    type ArchivedJsonValue,
    type LegacyArchiveProfile,
} from '@sammo-ts/common';

export { isLegacyArchiveProfile, LEGACY_ARCHIVE_PROFILES, type LegacyArchiveProfile } from '@sammo-ts/common';

import {
    paginateSource,
    jsonParameter,
    sourceMaxId,
    toDate,
    toFloat,
    toNullableDate,
    toNullableString,
    toNumber,
    toStringValue,
    upsertRows,
    withMigrationLock,
    type SourceRow,
    type TargetRow,
} from './db.js';
import type { MigrationSummary } from './gateway.js';
import {
    defaultExecutionOptions,
    loadCheckpoint,
    requireIncrementalCheckpoint,
    saveCheckpoint,
    validateSourceIdentity,
    type MigrationExecutionOptions,
    type MigrationProgress,
} from './incremental.js';
import { legacyUserId } from './identity.js';
import {
    classifyGameStorage,
    parseInheritanceValue,
    parseJson,
    parseStorageUserId,
    type JsonValue,
} from './transform.js';

const batchSize = 250;

interface ArchiveMigrationContext {
    profile: LegacyArchiveProfile;
    importRunId: string;
    sourceFormats: Record<ArchivedGeneralSourceFormat, number>;
}

interface AppendCursor {
    afterId: bigint;
    endAtId: bigint;
}

const parseNullableJson = (value: unknown, fallback: JsonValue, context: string): JsonValue =>
    value === null || value === undefined ? fallback : parseJson(value, context);

const parseJsonOrLegacyEmpty = (value: unknown, context: string): JsonValue =>
    value === '' ? '' : parseJson(value, context);

const ownerId = (value: unknown): string | null => {
    if (value === null || value === undefined) {
        return null;
    }
    const memberNo = toNumber(value, 'legacy owner');
    return memberNo > 0 ? legacyUserId(memberNo) : null;
};

const hashYearbook = (map: JsonValue, nations: JsonValue, globalHistory: JsonValue, globalAction: JsonValue): string =>
    createHash('sha256').update(JSON.stringify({ map, nations, globalHistory, globalAction })).digest('hex');

const asJsonRecord = (value: JsonValue): Record<string, JsonValue> =>
    value !== null && !Array.isArray(value) && typeof value === 'object' ? value : {};

export const resolveLegacyGameOpenedAt = (env: JsonValue, legacyDate: Date, context: string): Date => {
    const record = asJsonRecord(env);
    const candidate = record.opentime ?? record.starttime;
    if (candidate === null || candidate === undefined || candidate === '') {
        return legacyDate;
    }
    try {
        return toDate(candidate, context);
    } catch {
        // 후기 레거시 기수는 개장 전 sentinel(-324000000/0)을 기록하기도 한다.
        // raw_env는 그대로 보존하고, 유효한 ng_games.date만 파생 시각에 사용한다.
        return legacyDate;
    }
};

const migrateSimpleTable = async (
    source: MariaPool,
    target: PoolClient | null,
    sourceTable: string,
    sourceIdColumn: string,
    targetTable: string,
    conflictColumns: readonly string[],
    mapper: (row: SourceRow) => TargetRow,
    counts: Record<string, number>,
    progress: MigrationProgress,
    cursor: AppendCursor,
    size = batchSize,
    strategy: 'append' | 'rescan' = 'append'
): Promise<void> => {
    for await (const rows of paginateSource(source, sourceTable, sourceIdColumn, size, cursor.afterId)) {
        const mapped = rows.map(mapper);
        if (target) {
            await upsertRows(target, targetTable, mapped, conflictColumns);
        }
        counts[sourceTable] = (counts[sourceTable] ?? 0) + mapped.length;
    }
    progress[sourceTable] = {
        strategy,
        startAfterId: cursor.afterId < 0n ? null : cursor.afterId.toString(),
        endAtId: cursor.endAtId < 0n ? null : cursor.endAtId.toString(),
        processed: counts[sourceTable] ?? 0,
    };
};

const migrateHall = (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    archive: ArchiveMigrationContext,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> =>
    migrateSimpleTable(
        source,
        target,
        'hall',
        'id',
        'legacy_archive.hall',
        ['source_profile', 'server_id', 'type', 'general_no'],
        (row) => {
            const sourceId = toNumber(row.id, 'hall.id');
            return {
                source_profile: archive.profile,
                legacy_id: sourceId,
                server_id: toStringValue(row.server_id, `hall.${sourceId}.server_id`),
                season: toNumber(row.season, `hall.${sourceId}.season`),
                scenario: toNumber(row.scenario, `hall.${sourceId}.scenario`),
                general_no: toNumber(row.general_no, `hall.${sourceId}.general_no`),
                type: toStringValue(row.type, `hall.${sourceId}.type`),
                value: toFloat(row.value, `hall.${sourceId}.value`),
                owner: ownerId(row.owner),
                aux: parseJson(row.aux, `hall.${sourceId}.aux`),
                import_run_id: archive.importRunId,
            };
        },
        counts,
        progress,
        cursor
    );

const migrateGames = (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    archive: ArchiveMigrationContext,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> =>
    migrateSimpleTable(
        source,
        target,
        'ng_games',
        'id',
        'legacy_archive.game_history',
        ['source_profile', 'server_id'],
        (row) => {
            const sourceId = toNumber(row.id, 'ng_games.id');
            const legacyDate = toDate(row.date, `ng_games.${sourceId}.date`);
            const env = parseJson(row.env, `ng_games.${sourceId}.env`);
            return {
                source_profile: archive.profile,
                server_id: toStringValue(row.server_id, `ng_games.${sourceId}.server_id`),
                legacy_id: sourceId,
                opened_at: resolveLegacyGameOpenedAt(env, legacyDate, `ng_games.${sourceId}.opened_at`),
                completed_at: null,
                legacy_date: legacyDate,
                winner_nation:
                    row.winner_nation === null
                        ? null
                        : toNumber(row.winner_nation, `ng_games.${sourceId}.winner_nation`),
                map: toNullableString(row.map),
                season: toNumber(row.season, `ng_games.${sourceId}.season`),
                scenario: toNumber(row.scenario, `ng_games.${sourceId}.scenario`),
                scenario_name: toStringValue(row.scenario_name, `ng_games.${sourceId}.scenario_name`),
                raw_env: jsonParameter(env),
                import_run_id: archive.importRunId,
            };
        },
        counts,
        progress,
        cursor,
        batchSize,
        'rescan'
    );

const migrateOldGenerals = (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    archive: ArchiveMigrationContext,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> =>
    migrateSimpleTable(
        source,
        target,
        'ng_old_generals',
        'id',
        'legacy_archive.general',
        ['source_profile', 'server_id', 'general_no'],
        (row) => {
            const sourceId = toNumber(row.id, 'ng_old_generals.id');
            const name = toStringValue(row.name, `ng_old_generals.${sourceId}.name`);
            const rawData = parseJson(row.data, `ng_old_generals.${sourceId}.data`);
            const normalized = normalizeArchivedGeneral(rawData as ArchivedJsonValue, name);
            archive.sourceFormats[normalized.sourceFormat] += 1;
            return {
                source_profile: archive.profile,
                server_id: toStringValue(row.server_id, `ng_old_generals.${sourceId}.server_id`),
                general_no: toNumber(row.general_no, `ng_old_generals.${sourceId}.general_no`),
                legacy_id: sourceId,
                owner: ownerId(row.owner),
                name,
                last_yearmonth: toNumber(row.last_yearmonth, `ng_old_generals.${sourceId}.last_yearmonth`),
                turntime: toDate(row.turntime, `ng_old_generals.${sourceId}.turntime`),
                schema_version: normalized.snapshot.schemaVersion,
                source_format: normalized.sourceFormat,
                data: jsonParameter(normalized.snapshot),
                raw_data: jsonParameter(rawData),
                import_run_id: archive.importRunId,
            };
        },
        counts,
        progress,
        cursor
    );

const migrateOldNations = (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    archive: ArchiveMigrationContext,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> =>
    migrateSimpleTable(
        source,
        target,
        'ng_old_nations',
        'id',
        'legacy_archive.nation',
        ['source_profile', 'legacy_id'],
        (row) => {
            const sourceId = toNumber(row.id, 'ng_old_nations.id');
            return {
                source_profile: archive.profile,
                legacy_id: sourceId,
                server_id: toStringValue(row.server_id, `ng_old_nations.${sourceId}.server_id`),
                nation: toNumber(row.nation, `ng_old_nations.${sourceId}.nation`),
                data: jsonParameter(parseJson(row.data, `ng_old_nations.${sourceId}.data`)),
                archived_at: toDate(row.date, `ng_old_nations.${sourceId}.date`),
                import_run_id: archive.importRunId,
            };
        },
        counts,
        progress,
        cursor
    );

const migrateEmperors = (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    archive: ArchiveMigrationContext,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> =>
    migrateSimpleTable(
        source,
        target,
        'emperior',
        'no',
        'legacy_archive.emperor',
        ['source_profile', 'legacy_id'],
        (row) => {
            const id = toNumber(row.no, 'emperior.no');
            const data = {
                phase: toNullableString(row.phase),
                nation_count: toNullableString(row.nation_count),
                nation_name: toNullableString(row.nation_name),
                nation_hist: toNullableString(row.nation_hist),
                gen_count: toNullableString(row.gen_count),
                personal_hist: toNullableString(row.personal_hist),
                special_hist: toNullableString(row.special_hist),
                name: toNullableString(row.name),
                type: toNullableString(row.type),
                color: toNullableString(row.color),
                year: row.year === null ? null : toNumber(row.year, `emperior.${id}.year`),
                month: row.month === null ? null : toNumber(row.month, `emperior.${id}.month`),
                power: row.power === null ? null : toNumber(row.power, `emperior.${id}.power`),
                gennum: row.gennum === null ? null : toNumber(row.gennum, `emperior.${id}.gennum`),
                citynum: row.citynum === null ? null : toNumber(row.citynum, `emperior.${id}.citynum`),
                pop: toNullableString(row.pop),
                poprate: toNullableString(row.poprate),
                gold: row.gold === null ? null : toNumber(row.gold, `emperior.${id}.gold`),
                rice: row.rice === null ? null : toNumber(row.rice, `emperior.${id}.rice`),
                l12name: toNullableString(row.l12name),
                l12pic: toNullableString(row.l12pic),
                l11name: toNullableString(row.l11name),
                l11pic: toNullableString(row.l11pic),
                l10name: toNullableString(row.l10name),
                l10pic: toNullableString(row.l10pic),
                l9name: toNullableString(row.l9name),
                l9pic: toNullableString(row.l9pic),
                l8name: toNullableString(row.l8name),
                l8pic: toNullableString(row.l8pic),
                l7name: toNullableString(row.l7name),
                l7pic: toNullableString(row.l7pic),
                l6name: toNullableString(row.l6name),
                l6pic: toNullableString(row.l6pic),
                l5name: toNullableString(row.l5name),
                l5pic: toNullableString(row.l5pic),
                tiger: toNullableString(row.tiger),
                eagle: toNullableString(row.eagle),
                gen: toNullableString(row.gen),
                history: parseNullableJson(row.history, [], `emperior.${id}.history`),
                aux: parseNullableJson(row.aux, {}, `emperior.${id}.aux`),
            };
            return {
                source_profile: archive.profile,
                legacy_id: id,
                server_id: toNullableString(row.server_id),
                data: jsonParameter(data),
                import_run_id: archive.importRunId,
            };
        },
        counts,
        progress,
        cursor
    );

const migrateInheritanceResults = (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> =>
    migrateSimpleTable(
        source,
        target,
        'inheritance_result',
        'id',
        'inheritance_result',
        ['legacy_id'],
        (row) => {
            const id = toNumber(row.id, 'inheritance_result.id');
            return {
                legacy_id: id,
                server_id: toStringValue(row.server_id, `inheritance_result.${id}.server_id`),
                owner: legacyUserId(toNumber(row.owner, `inheritance_result.${id}.owner`)),
                general_id: toNumber(row.general_id, `inheritance_result.${id}.general_id`),
                year: toNumber(row.year, `inheritance_result.${id}.year`),
                month: toNumber(row.month, `inheritance_result.${id}.month`),
                value: jsonParameter(parseJsonOrLegacyEmpty(row.value, `inheritance_result.${id}.value`)),
                created_at: new Date(0),
            };
        },
        counts,
        progress,
        cursor
    );

const migrateUserRecords = (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> =>
    migrateSimpleTable(
        source,
        target,
        'user_record',
        'id',
        'inheritance_log',
        ['legacy_id'],
        (row) => {
            const id = toNumber(row.id, 'user_record.id');
            return {
                legacy_id: id,
                user_id: legacyUserId(toNumber(row.user_id, `user_record.${id}.user_id`)),
                server_id: toStringValue(row.server_id, `user_record.${id}.server_id`),
                log_type: toStringValue(row.log_type, `user_record.${id}.log_type`),
                year: toNumber(row.year, `user_record.${id}.year`),
                month: toNumber(row.month, `user_record.${id}.month`),
                text: toStringValue(row.text, `user_record.${id}.text`),
                created_at: toNullableDate(row.date, `user_record.${id}.date`) ?? new Date(0),
            };
        },
        counts,
        progress,
        cursor
    );

const migrateYearbook = async (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>,
    archive: ArchiveMigrationContext,
    progress: MigrationProgress,
    cursor: AppendCursor
): Promise<void> => {
    await migrateSimpleTable(
        source,
        target,
        'ng_history',
        'no',
        'legacy_archive.yearbook',
        ['source_profile', 'legacy_id'],
        (row) => {
            const id = toNumber(row.no, 'ng_history.no');
            const map = parseNullableJson(row.map, {}, `ng_history.${id}.map`);
            const nations = parseNullableJson(row.nations, [], `ng_history.${id}.nations`);
            const globalHistory = parseNullableJson(row.global_history, [], `ng_history.${id}.global_history`);
            const globalAction = parseNullableJson(row.global_action, [], `ng_history.${id}.global_action`);
            const mapped: TargetRow = {
                source_profile: archive.profile,
                legacy_id: id,
                profile_name: toStringValue(row.server_id, `ng_history.${id}.server_id`),
                year: toNumber(row.year, `ng_history.${id}.year`),
                month: toNumber(row.month, `ng_history.${id}.month`),
                map: jsonParameter(map),
                nations: jsonParameter(nations),
                global_history: jsonParameter(globalHistory),
                global_action: jsonParameter(globalAction),
                content_hash: hashYearbook(map, nations, globalHistory, globalAction),
                import_run_id: archive.importRunId,
            };
            return mapped;
        },
        counts,
        progress,
        cursor,
        25
    );
};

const migrateStorage = async (
    source: MariaPool,
    target: PoolClient | null,
    counts: Record<string, number>
): Promise<void> => {
    for await (const rows of paginateSource(source, 'storage', 'id', batchSize)) {
        const archives: TargetRow[] = [];
        const points: TargetRow[] = [];
        const userStates: TargetRow[] = [];
        for (const row of rows) {
            const sourceId = toNumber(row.id, 'storage.id');
            const namespace = toStringValue(row.namespace, `storage.${sourceId}.namespace`);
            const key = toStringValue(row.key, `storage.${sourceId}.key`);
            const value = parseJson(row.value, `storage.${sourceId}.value`);
            const scope = classifyGameStorage(namespace, key);
            counts.storage_inspected = (counts.storage_inspected ?? 0) + 1;
            if (scope === 'season-state') {
                counts.storage_season_excluded = (counts.storage_season_excluded ?? 0) + 1;
                continue;
            }
            archives.push({ source_id: sourceId, namespace, key, value: jsonParameter(value), scope });

            const inheritanceUserId = parseStorageUserId(namespace, 'inheritance');
            if (inheritanceUserId) {
                points.push({
                    user_id: inheritanceUserId,
                    key,
                    value: parseInheritanceValue(value, `storage.${sourceId}.value`),
                    aux: {
                        legacyNamespace: namespace,
                        legacySourceId: sourceId,
                        legacyAux: Array.isArray(value) ? (value[1] ?? null) : null,
                    },
                    updated_at: new Date(0),
                });
            }
            const stateUserId = parseStorageUserId(namespace, 'user');
            if (stateUserId && key === 'last_stat_reset') {
                userStates.push({
                    user_id: stateUserId,
                    meta: { lastStatReset: value, legacySourceId: sourceId },
                    updated_at: new Date(0),
                });
            }
        }
        if (target) {
            // Ref storage는 같은 namespace/key를 삭제 후 새 auto-increment ID로 다시 만들 수 있다.
            // 장기 상태의 권위 identity로 자연키를 사용하고 최신 source ID까지 함께 갱신한다.
            await upsertRows(target, 'legacy_game_storage', archives, ['namespace', 'key']);
            await upsertRows(target, 'inheritance_point', points, ['user_id', 'key']);
            await upsertRows(target, 'inheritance_user_state', userStates, ['user_id']);
        }
        counts.storage_archived = (counts.storage_archived ?? 0) + archives.length;
        counts.inheritance_point = (counts.inheritance_point ?? 0) + points.length;
        counts.inheritance_user_state = (counts.inheritance_user_state ?? 0) + userStates.length;
    }
};

export const migrateGame = async (
    source: MariaPool,
    targetPool: PgPool | null,
    apply: boolean,
    profile: string,
    execution: MigrationExecutionOptions = defaultExecutionOptions(`legacy-game-${profile}`)
): Promise<MigrationSummary> => {
    if (!isLegacyArchiveProfile(profile)) {
        throw new Error(`Unsupported legacy archive profile: ${profile}`);
    }
    validateSourceIdentity(execution.source);
    if (execution.mode === 'incremental' && !targetPool) {
        throw new Error('Incremental game migration requires the target database to read checkpoints');
    }
    const counts: Record<string, number> = {};
    const progress: MigrationProgress = {};
    const sourceFormats: Record<ArchivedGeneralSourceFormat, number> = {
        'legacy-flat-v0': 0,
        'ref-flat-v1': 0,
        'core-snapshot-v1': 0,
        unknown: 0,
    };
    const excluded = {
        general: 'Current-season actor state is intentionally not transferred.',
        city: 'Current-season world state is intentionally not transferred.',
        nation: 'Current-season nation state is intentionally not transferred.',
        general_turn: 'Current-season command queue.',
        general_access_log: 'Current-season access counters.',
        nation_turn: 'Current-season nation command queue.',
        nation_env: 'Current-season nation KV state.',
        board: 'Current-season nation board.',
        comment: 'Current-season nation board comments.',
        diplomacy: 'Current-season diplomacy state.',
        event: 'Current-season scheduled events.',
        message: 'Current-season mailboxes.',
        rank_data: 'Current-season ranking counters.',
        statistic: 'Current-season statistics used to build permanent dynasty records.',
        world_history: 'Current-season history; completed-month snapshots come from ng_history.',
        general_record: 'Current-season general logs.',
        ng_auction: 'Current-season auction.',
        ng_auction_bid: 'Current-season auction bids.',
        ng_betting: 'Current-season betting.',
        ng_diplomacy: 'Current-season diplomacy letters.',
        plock: 'Legacy process lock.',
        reserved_open: 'Legacy opening schedule.',
        select_npc_token: 'Ephemeral selection token.',
        select_pool: 'Current-season selection pool.',
        tournament: 'Current-season tournament.',
        troop: 'Current-season troop state.',
        vote: 'Current-season vote.',
        vote_comment: 'Current-season vote comments.',
        'storage:season-state': 'Only inheritance_* and user_* long-lived namespaces are archived or projected.',
    };
    const client = targetPool ? await targetPool.connect() : null;
    let importRunId: string | null = null;
    try {
        const run = async (archive: ArchiveMigrationContext): Promise<void> => {
            const writeClient = apply ? client : null;
            const cursorSpecs = [
                ['hall', 'id'],
                ['ng_old_generals', 'id'],
                ['ng_old_nations', 'id'],
                ['emperior', 'no'],
                ['inheritance_result', 'id'],
                ['user_record', 'id'],
                ['ng_history', 'no'],
            ] as const;
            const cursors = new Map<string, AppendCursor>();
            for (const [sourceTable, idColumn] of cursorSpecs) {
                const endAtId = (await sourceMaxId(source, sourceTable, idColumn)) ?? -1n;
                const checkpoint =
                    execution.mode === 'incremental' && client
                        ? await loadCheckpoint(
                              client,
                              {
                                  tableSql: '"legacy_archive"."import_checkpoint"',
                                  scope: { columnSql: '"source_profile"', value: profile },
                              },
                              execution.source.key,
                              sourceTable
                          )
                        : null;
                const afterId =
                    execution.mode === 'incremental'
                        ? requireIncrementalCheckpoint(checkpoint, execution.source, sourceTable)
                        : -1n;
                if (endAtId < afterId) {
                    throw new Error(`Legacy source ${sourceTable} maximum ID regressed; run a reviewed full migration`);
                }
                cursors.set(sourceTable, { afterId, endAtId });
            }
            const cursor = (table: string): AppendCursor => {
                const value = cursors.get(table);
                if (!value) throw new Error(`Missing migration cursor for ${table}`);
                return value;
            };
            const maxGameId = (await sourceMaxId(source, 'ng_games', 'id')) ?? -1n;
            await migrateGames(source, writeClient, counts, archive, progress, { afterId: -1n, endAtId: maxGameId });
            await migrateHall(source, writeClient, counts, archive, progress, cursor('hall'));
            await migrateOldGenerals(source, writeClient, counts, archive, progress, cursor('ng_old_generals'));
            await migrateOldNations(source, writeClient, counts, archive, progress, cursor('ng_old_nations'));
            await migrateEmperors(source, writeClient, counts, archive, progress, cursor('emperior'));
            await migrateInheritanceResults(source, writeClient, counts, progress, cursor('inheritance_result'));
            await migrateUserRecords(source, writeClient, counts, progress, cursor('user_record'));
            await migrateStorage(source, writeClient, counts);
            progress.storage = {
                strategy: 'rescan',
                startAfterId: null,
                endAtId: null,
                processed: counts.storage_inspected ?? 0,
            };
            await migrateYearbook(source, writeClient, counts, archive, progress, cursor('ng_history'));
            if (apply && client && archive.importRunId !== '0') {
                for (const [sourceTable] of cursorSpecs) {
                    await saveCheckpoint(
                        client,
                        {
                            tableSql: '"legacy_archive"."import_checkpoint"',
                            scope: { columnSql: '"source_profile"', value: profile },
                        },
                        execution.source,
                        sourceTable,
                        cursor(sourceTable).endAtId,
                        archive.importRunId
                    );
                }
            }
        };
        if (client && apply) {
            await withMigrationLock(client, `sammo-legacy-archive-v3:${profile}:${execution.source.key}`, async () => {
                const created = await client.query<{ id: string }>(
                    `INSERT INTO "legacy_archive"."import_run"
                            ("source_profile", "source_key", "source_fingerprint", "mode", "status")
                         VALUES ($1, $2, $3, $4, 'RUNNING') RETURNING "id"`,
                    [profile, execution.source.key, execution.source.fingerprint, execution.mode]
                );
                importRunId = created.rows[0]?.id ?? null;
                if (!importRunId) throw new Error('Failed to create legacy archive import run');
                const archive: ArchiveMigrationContext = { profile, importRunId, sourceFormats };
                await client.query('BEGIN');
                try {
                    await run(archive);
                    await client.query(
                        `UPDATE "legacy_archive"."import_run"
                             SET "status" = 'COMPLETED', "finished_at" = CURRENT_TIMESTAMP,
                                 "counts" = $2::jsonb, "source_format_summary" = $3::jsonb,
                                 "progress" = $4::jsonb
                            WHERE "id" = $1`,
                        [importRunId, JSON.stringify(counts), JSON.stringify(sourceFormats), JSON.stringify(progress)]
                    );
                    await client.query('COMMIT');
                } catch (error) {
                    await client.query('ROLLBACK');
                    const message =
                        error instanceof Error ? error.message.slice(0, 2000) : String(error).slice(0, 2000);
                    await client.query(
                        `UPDATE "legacy_archive"."import_run"
                             SET "status" = 'FAILED', "finished_at" = CURRENT_TIMESTAMP,
                                 "counts" = $2::jsonb, "source_format_summary" = $3::jsonb,
                                 "progress" = $4::jsonb, "error" = $5
                             WHERE "id" = $1`,
                        [
                            importRunId,
                            JSON.stringify(counts),
                            JSON.stringify(sourceFormats),
                            JSON.stringify(progress),
                            message,
                        ]
                    );
                    throw error;
                }
            });
        } else {
            await run({ profile, importRunId: '0', sourceFormats });
        }
    } finally {
        client?.release();
    }
    return {
        command: 'game',
        apply,
        counts,
        excluded,
        importRunId,
        sourceFormatSummary: sourceFormats,
        mode: execution.mode,
        sourceKey: execution.source.key,
        progress,
    };
};
