import { LiteHashDRBG, RandUtil, type RNG } from '@sammo-ts/common';
import { readLegacyStoredFloat } from '@sammo-ts/logic/compat/legacyFloat.js';
import {
    GENERAL_TURN_COMMAND_KEYS,
    NATION_TURN_COMMAND_KEYS,
    normalizeScenarioEffect,
    readLegacyCityTrust,
    type MapDefinition,
    type Nation,
    type TurnCommandProfile,
    type UnitSetDefinition,
} from '@sammo-ts/logic';
import { InMemoryTurnWorld } from '@sammo-ts/game-engine/turn/inMemoryWorld.js';
import { createReservedTurnHandler } from '@sammo-ts/game-engine/turn/reservedTurnHandler.js';
import { InMemoryReservedTurnStore } from '@sammo-ts/game-engine/turn/reservedTurnStore.js';
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
import { loadMapDefinitionByName } from '@sammo-ts/game-engine/scenario/mapLoader.js';
import type {
    TurnDiplomacy,
    TurnGeneral,
    TurnWorldSnapshot,
    TurnWorldState,
} from '@sammo-ts/game-engine/turn/types.js';
import { applyPersistedRankRowsToMeta, buildLegacyComparableRankRows } from '@sammo-ts/game-engine/turn/rankData.js';

import {
    canonicalizeTurnCommandArgs,
    type CanonicalTurnCommandTrace,
    type CanonicalTurnSnapshot,
} from './canonical.js';

interface GeneralCooldownSelector {
    generalId: number;
    actionName: string;
}

interface NationCooldownSelector {
    nationId: number;
    actionName: string;
}

export interface TurnCommandFixtureRequest {
    kind: 'general' | 'nation';
    actorGeneralId: number;
    action: string;
    includeLifecycle?: boolean;
    args?: unknown;
    coreArgs?: unknown;
    setup?: {
        world?: {
            startYear?: number;
            initYear?: number;
            initMonth?: number;
            year?: number;
            month?: number;
            hiddenSeed?: string;
            scenarioEffect?: string | null;
            staticEventHandlers?: Record<string, string[]>;
        };
        isolateWorld?: boolean;
        generals?: Array<Record<string, unknown>>;
        rankData?: Array<{ generalId: number; type: string; value: number }>;
        nations?: Array<Record<string, unknown>>;
        cities?: Array<Record<string, unknown>>;
        troops?: Array<Record<string, unknown>>;
        diplomacy?: Array<Record<string, unknown>>;
        generalTurns?: Array<Record<string, unknown>>;
        nationTurns?: Array<Record<string, unknown>>;
        randomFoundingCandidateCityIds?: number[];
        generalCooldowns?: Array<GeneralCooldownSelector & { nextAvailableTurn: number }>;
    };
    observe?: {
        generalIds?: number[];
        cityIds?: number[];
        nationIds?: number[];
        logAfterId?: number;
        messageAfterId?: number;
        includeNationHistoryLogs?: boolean;
        includeGlobalHistoryLogs?: boolean;
        generalCooldowns?: GeneralCooldownSelector[];
        nationCooldowns?: NationCooldownSelector[];
        diplomacyPairs?: Array<{ fromNationId: number; toNationId: number }>;
    };
}

interface RandomCall {
    seq: number;
    operation: string;
    arguments: Record<string, unknown>;
    result: unknown;
}

class TracingRng implements RNG {
    public readonly calls: RandomCall[] = [];

    public constructor(private readonly inner: RNG) {}

    public getMaxInt(): number {
        return this.inner.getMaxInt();
    }

    public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
        const result = this.inner.nextBytes(bytes);
        this.record('nextBytes', { bytes }, Buffer.from(result).toString('hex'));
        return result;
    }

    public nextBits(bits: number): Uint8Array<ArrayBuffer> {
        const result = this.inner.nextBits(bits);
        this.record('nextBits', { bits }, Buffer.from(result).toString('hex'));
        return result;
    }

    public nextInt(max?: number): number {
        const result = this.inner.nextInt(max);
        this.record('nextInt', { maxInclusive: max ?? null }, result);
        return result;
    }

    public nextFloat1(): number {
        const result = this.inner.nextFloat1();
        this.record('nextFloat1', {}, result);
        return result;
    }

    private record(operation: string, args: Record<string, unknown>, result: unknown): void {
        this.calls.push({
            seq: this.calls.length,
            operation,
            arguments: args,
            result,
        });
    }
}

const asRecord = (value: unknown): Record<string, unknown> =>
    typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
const asNumberRecord = (value: unknown): Record<string, number> =>
    Object.fromEntries(
        Object.entries(asRecord(value)).filter((entry): entry is [string, number] => typeof entry[1] === 'number')
    );

const readNumber = (record: Record<string, unknown>, key: string, fallback = 0): number => {
    const value = record[key];
    if (typeof value === 'number' && Number.isFinite(value)) {
        return value;
    }
    if (typeof value === 'string') {
        const parsed = Number(value);
        if (Number.isFinite(parsed)) {
            return parsed;
        }
    }
    return fallback;
};

const readString = (record: Record<string, unknown>, key: string, fallback: string): string => {
    const value = record[key];
    return typeof value === 'string' ? value : fallback;
};

const readNullableString = (record: Record<string, unknown>, key: string): string | null => {
    const value = record[key];
    return typeof value === 'string' && value !== '' && value !== 'None' ? value : null;
};

const toDatabaseInt = (value: number): number => Math.round(value);

const COMMANDS_WITH_LEGACY_CORE_ARG_KEYS = new Set([
    'che_장수대상임관',
    'che_선양',
    'che_증여',
    'che_천도',
    'che_몰수',
]);

export const resolveCoreTurnCommandArgs = (request: TurnCommandFixtureRequest): Record<string, unknown> => {
    const explicit = request.coreArgs;
    if (explicit !== undefined) {
        return asRecord(explicit);
    }
    if (COMMANDS_WITH_LEGACY_CORE_ARG_KEYS.has(request.action)) {
        return asRecord(request.args);
    }
    return asRecord(canonicalizeTurnCommandArgs(request.args ?? {}));
};

export const createCoreTurnCommandProfile = (request: TurnCommandFixtureRequest): TurnCommandProfile => {
    if (request.kind === 'general') {
        if (!GENERAL_TURN_COMMAND_KEYS.includes(request.action as (typeof GENERAL_TURN_COMMAND_KEYS)[number])) {
            throw new Error(`Unknown general command: ${request.action}`);
        }
        const generalActions = [request.action, '휴식', 'che_인재탐색', 'che_해산', 'che_이동'] as Array<
            (typeof GENERAL_TURN_COMMAND_KEYS)[number]
        >;
        return {
            general: [...new Set(generalActions)],
            nation: ['휴식'],
        };
    }
    if (!NATION_TURN_COMMAND_KEYS.includes(request.action as (typeof NATION_TURN_COMMAND_KEYS)[number])) {
        throw new Error(`Unknown nation command: ${request.action}`);
    }
    return {
        general: ['휴식'],
        nation: [request.action as (typeof NATION_TURN_COMMAND_KEYS)[number], '휴식'],
    };
};

const buildGeneral = (row: Record<string, unknown>, fallbackTurnTime: Date): TurnGeneral => {
    const meta = asRecord(row.meta);
    const rawTurnTime = row.turnTime;
    const parsedTurnTime = typeof rawTurnTime === 'string' ? new Date(rawTurnTime) : fallbackTurnTime;
    const turnTime = Number.isNaN(parsedTurnTime.getTime()) ? fallbackTurnTime : parsedTurnTime;
    const rawLastTurn = asRecord(row.lastTurn);
    const lastTurn =
        typeof rawLastTurn.command === 'string'
            ? {
                  command: rawLastTurn.command,
                  ...(typeof rawLastTurn.term === 'number' ? { term: rawLastTurn.term } : {}),
                  ...(typeof rawLastTurn.seq === 'number' ? { seq: rawLastTurn.seq } : {}),
                  ...(Object.keys(asRecord(rawLastTurn.arg)).length > 0 ? { arg: asRecord(rawLastTurn.arg) } : {}),
              }
            : undefined;
    return {
        id: readNumber(row, 'id'),
        name: readString(row, 'name', '장수'),
        nationId: readNumber(row, 'nationId'),
        cityId: readNumber(row, 'cityId'),
        troopId: readNumber(row, 'troopId'),
        stats: {
            leadership: readNumber(row, 'leadership', 80),
            strength: readNumber(row, 'strength', 70),
            intelligence: readNumber(row, 'intelligence', 60),
        },
        experience: readNumber(row, 'experience'),
        dedication: readNumber(row, 'dedication'),
        officerLevel: readNumber(row, 'officerLevel', 1),
        role: {
            personality: readNullableString(row, 'personality'),
            specialDomestic: readNullableString(row, 'specialDomestic'),
            specialWar: readNullableString(row, 'specialWar'),
            items: {
                horse: readNullableString(row, 'itemHorse'),
                weapon: readNullableString(row, 'itemWeapon'),
                book: readNullableString(row, 'itemBook'),
                item: readNullableString(row, 'itemExtra'),
            },
        },
        injury: readNumber(row, 'injury'),
        gold: readNumber(row, 'gold'),
        rice: readNumber(row, 'rice'),
        crew: readNumber(row, 'crew'),
        crewTypeId: readNumber(row, 'crewTypeId', 1100),
        train: readNumber(row, 'train'),
        atmos: readNumber(row, 'atmos'),
        age: readNumber(row, 'age', 30),
        npcState: readNumber(row, 'npcState'),
        userId: row.hasOwner === true ? 'turn-differential-owner' : null,
        penalty: row.penalty,
        triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
        meta: {
            ...meta,
            specage: readNumber(row, 'specAge', readNumber(meta, 'specage')),
            specage2: readNumber(row, 'specAge2', readNumber(meta, 'specage2')),
            makelimit: readNumber(row, 'makeLimit', readNumber(meta, 'makelimit')),
            killturn: readNumber(row, 'killTurn', readNumber(meta, 'killturn', 24)),
            myset: readNumber(row, 'mySet', readNumber(meta, 'myset')),
            leadership_exp: readNumber(row, 'leadershipExp', readNumber(meta, 'leadership_exp')),
            strength_exp: readNumber(row, 'strengthExp', readNumber(meta, 'strength_exp')),
            intel_exp: readNumber(row, 'intelExp', readNumber(meta, 'intel_exp')),
            dex1: readNumber(row, 'dex1', readNumber(meta, 'dex1')),
            dex2: readNumber(row, 'dex2', readNumber(meta, 'dex2')),
            dex3: readNumber(row, 'dex3', readNumber(meta, 'dex3')),
            dex4: readNumber(row, 'dex4', readNumber(meta, 'dex4')),
            dex5: readNumber(row, 'dex5', readNumber(meta, 'dex5')),
            explevel: readNumber(row, 'expLevel', readNumber(meta, 'explevel')),
            betray: readNumber(row, 'betray', readNumber(meta, 'betray')),
            officerCityId: readNumber(
                row,
                'officerCityId',
                readNumber(meta, 'officerCityId', readNumber(meta, 'officerCity', readNumber(meta, 'officer_city')))
            ),
            officerCity: readNumber(
                row,
                'officerCityId',
                readNumber(meta, 'officerCity', readNumber(meta, 'officer_city'))
            ),
            officer_city: readNumber(
                row,
                'officerCityId',
                readNumber(meta, 'officer_city', readNumber(meta, 'officerCity'))
            ),
            belong: readNumber(row, 'belong', readNumber(meta, 'belong')),
            permission: readString(row, 'permission', readString(meta, 'permission', 'normal')),
            block: readNumber(row, 'blockState', readNumber(meta, 'block')),
        },
        ...(lastTurn ? { lastTurn } : {}),
        turnTime,
        recentWarTime: null,
    };
};

const buildNation = (row: Record<string, unknown>, generals: TurnGeneral[]): Nation => {
    const id = readNumber(row, 'id');
    const meta = asRecord(row.meta);
    const turnLastByOfficerLevel = asRecord(row.turnLastByOfficerLevel);
    return {
        id,
        name: readString(row, 'name', `국가${id}`),
        color: readString(row, 'color', '#777777'),
        capitalCityId: readNumber(row, 'capitalCityId'),
        chiefGeneralId: generals.find((general) => general.nationId === id && general.officerLevel === 12)?.id ?? null,
        gold: readNumber(row, 'gold'),
        rice: readNumber(row, 'rice'),
        power: readNumber(row, 'power'),
        level: readNumber(row, 'level', 1),
        typeCode: readString(row, 'typeCode', 'che_중립'),
        meta: {
            ...meta,
            ...Object.fromEntries(
                Object.entries(turnLastByOfficerLevel).map(([officerLevel, lastTurn]) => [
                    `turn_last_${officerLevel}`,
                    lastTurn,
                ])
            ),
            tech: readNumber(row, 'tech', readNumber(meta, 'tech')),
            gennum: readNumber(row, 'generalCount', readNumber(meta, 'gennum')),
            war: readNumber(row, 'war', readNumber(meta, 'war')),
            surlimit: readNumber(row, 'diplomacyLimit', readNumber(meta, 'surlimit')),
            capset: readNumber(row, 'capitalRevision', readNumber(meta, 'capset')),
            strategic_cmd_limit: readNumber(row, 'strategicCommandLimit', readNumber(meta, 'strategic_cmd_limit')),
        },
    };
};

export const buildCoreTurnCommandWorldInput = (
    request: TurnCommandFixtureRequest,
    referenceBefore: CanonicalTurnSnapshot,
    unitSet: UnitSetDefinition,
    map: MapDefinition
): { state: TurnWorldState; snapshot: TurnWorldSnapshot; map: MapDefinition } => {
    const year = readNumber(referenceBefore.world, 'year', request.setup?.world?.year ?? 185);
    const month = readNumber(referenceBefore.world, 'month', request.setup?.world?.month ?? 1);
    const turnTime = new Date(`${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-01T00:00:00.000Z`);
    const generals = referenceBefore.generals.map((row) => buildGeneral(row, turnTime));
    for (const general of generals) {
        applyPersistedRankRowsToMeta(
            general.meta,
            referenceBefore.rankData
                .filter((row) => readNumber(row, 'generalId') === general.id)
                .map((row) => ({
                    type: readString(row, 'type', ''),
                    value: readNumber(row, 'value'),
                }))
        );
    }
    const referenceGeneralCooldowns = Array.isArray(referenceBefore.world.generalCooldowns)
        ? referenceBefore.world.generalCooldowns
        : [];
    for (const rawCooldown of referenceGeneralCooldowns) {
        const cooldown = asRecord(rawCooldown);
        const generalId = readNumber(cooldown, 'generalId');
        const actionName = readString(cooldown, 'actionName', '');
        const nextAvailableTurn = cooldown.nextAvailableTurn;
        const general = generals.find((entry) => entry.id === generalId);
        if (general && actionName && typeof nextAvailableTurn === 'number' && Number.isFinite(nextAvailableTurn)) {
            general.meta[`next_execute_${actionName}`] = nextAvailableTurn;
        }
    }
    const fixtureNations = new Map((request.setup?.nations ?? []).map((row) => [readNumber(row, 'id'), row] as const));
    const nations = referenceBefore.nations.map((row) =>
        buildNation(
            {
                ...row,
                turnLastByOfficerLevel:
                    fixtureNations.get(readNumber(row, 'id'))?.coreTurnLastByOfficerLevel ??
                    fixtureNations.get(readNumber(row, 'id'))?.turnLastByOfficerLevel,
            },
            generals
        )
    );
    const referenceNationCooldowns = Array.isArray(referenceBefore.world.nationCooldowns)
        ? referenceBefore.world.nationCooldowns
        : [];
    for (const rawCooldown of referenceNationCooldowns) {
        const cooldown = asRecord(rawCooldown);
        const nationId = readNumber(cooldown, 'nationId');
        const actionName = readString(cooldown, 'actionName', '');
        const nextAvailableTurn = cooldown.nextAvailableTurn;
        const nation = nations.find((entry) => entry.id === nationId);
        if (nation && actionName && typeof nextAvailableTurn === 'number' && Number.isFinite(nextAvailableTurn)) {
            nation.meta[`next_execute_${actionName}`] = nextAvailableTurn;
        }
    }
    const observedCityRows = new Map(referenceBefore.cities.map((row) => [readNumber(row, 'id'), row] as const));
    const fixtureCityRows = new Map((request.setup?.cities ?? []).map((row) => [readNumber(row, 'id'), row] as const));
    const randomFoundingCandidateCityIds = request.setup?.randomFoundingCandidateCityIds
        ? new Set(request.setup.randomFoundingCandidateCityIds)
        : null;
    const diplomacy: TurnDiplomacy[] = referenceBefore.diplomacy.map((row) => ({
        fromNationId: readNumber(row, 'fromNationId'),
        toNationId: readNumber(row, 'toNationId'),
        state: readNumber(row, 'state', 3),
        term: readNumber(row, 'term'),
        dead: readNumber(row, 'dead'),
        meta: {},
    }));
    const snapshot: TurnWorldSnapshot = {
        scenarioConfig: {
            stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 75, npcMin: 10, chiefMin: 70 },
            iconPath: '',
            map: {},
            const: {
                develCost: readNumber(referenceBefore.world, 'develCost'),
                trainDelta: 30,
                atmosDelta: 30,
                maxTrainByCommand: 100,
                maxAtmosByCommand: 100,
                openingPartYear: 3,
                initialNationGenLimit: 10,
                maxGeneral: 500,
                baseGold: 0,
                baseRice: 2_000,
                generalMinimumGold: 0,
                generalMinimumRice: 500,
                npcSeizureMessageProb: 0.01,
                maxResourceActionAmount: 10_000,
                maxTechLevel: 12,
                maxLevel: 255,
                maxDedLevel: 30,
                upgradeLimit: 30,
                ...(request.setup?.world?.staticEventHandlers
                    ? { staticEventHandlers: request.setup.world.staticEventHandlers }
                    : {}),
            },
            environment: {
                mapName: map.id,
                unitSet: unitSet.id,
                ...(request.setup?.world?.scenarioEffect !== undefined
                    ? { scenarioEffect: normalizeScenarioEffect(request.setup.world.scenarioEffect) }
                    : {}),
            },
        },
        scenarioMeta: {
            title: '턴 명령 차등',
            startYear: request.setup?.world?.startYear ?? Math.max(1, year - 5),
            life: null,
            fiction: 0,
            history: [],
            ignoreDefaultEvents: false,
        },
        map,
        unitSet,
        nations,
        cities: map.cities.map((definition) => {
            const row = observedCityRows.get(definition.id) ?? {};
            const fixtureRow = fixtureCityRows.get(definition.id) ?? {};
            const conflictEntries = Array.isArray(fixtureRow.conflictEntries)
                ? fixtureRow.conflictEntries.filter(
                      (entry): entry is [number, number] =>
                          Array.isArray(entry) &&
                          entry.length === 2 &&
                          typeof entry[0] === 'number' &&
                          Number.isInteger(entry[0]) &&
                          typeof entry[1] === 'number'
                  )
                : [];
            const conflict =
                conflictEntries.length > 0 ? Object.fromEntries(conflictEntries) : asNumberRecord(row.conflict);
            return {
                id: definition.id,
                name: readString(row, 'name', definition.name),
                nationId: readNumber(row, 'nationId'),
                level: observedCityRows.has(definition.id)
                    ? readNumber(row, 'level', definition.level)
                    : randomFoundingCandidateCityIds === null
                      ? definition.level
                      : randomFoundingCandidateCityIds.has(definition.id)
                        ? 5
                        : 4,
                state: readNumber(row, 'state'),
                population: readNumber(row, 'population', definition.initial.population),
                populationMax: readNumber(row, 'populationMax', definition.max.population),
                agriculture: readNumber(row, 'agriculture', definition.initial.agriculture),
                agricultureMax: readNumber(row, 'agricultureMax', definition.max.agriculture),
                commerce: readNumber(row, 'commerce', definition.initial.commerce),
                commerceMax: readNumber(row, 'commerceMax', definition.max.commerce),
                security: readNumber(row, 'security', definition.initial.security),
                securityMax: readNumber(row, 'securityMax', definition.max.security),
                supplyState: readNumber(row, 'supplyState', map.defaults?.supplyState ?? 1),
                frontState: readNumber(row, 'frontState', map.defaults?.frontState ?? 0),
                defence: readNumber(row, 'defence', definition.initial.defence),
                defenceMax: readNumber(row, 'defenceMax', definition.max.defence),
                wall: readNumber(row, 'wall', definition.initial.wall),
                wallMax: readNumber(row, 'wallMax', definition.max.wall),
                conflict,
                meta: {
                    trust: readNumber(row, 'trust', map.defaults?.trust ?? 50),
                    trade: readNumber(row, 'trade', map.defaults?.trade ?? 100),
                    term: readNumber(row, 'term'),
                    officer_set: readNumber(row, 'officerSet'),
                    ...(conflictEntries.length > 0
                        ? { conflict_order: conflictEntries.map(([nationId]) => nationId) }
                        : {}),
                },
            };
        }),
        generals,
        troops:
            request.setup?.troops?.map((row) => {
                const id = readNumber(row, 'id');
                return {
                    id,
                    nationId: readNumber(row, 'nationId'),
                    name: readString(row, 'name', `부대${id}`),
                };
            }) ?? [],
        diplomacy,
        events: [],
        initialEvents: [],
    };
    return {
        state: {
            id: 1,
            currentYear: year,
            currentMonth: month,
            tickSeconds: readNumber(referenceBefore.world, 'tickMinutes', 10) * 60,
            lastTurnTime: turnTime,
            meta: {
                hiddenSeed: request.setup?.world?.hiddenSeed ?? 'turn-command-differential-seed',
                killturn: readNumber(referenceBefore.world, 'killTurn', 24),
                isUnited: readNumber(referenceBefore.world, 'isUnited'),
                scenarioId: readNumber(referenceBefore.world, 'scenarioId'),
                initYear: readNumber(
                    referenceBefore.world,
                    'initYear',
                    request.setup?.world?.initYear ?? request.setup?.world?.startYear ?? year
                ),
                initMonth: readNumber(referenceBefore.world, 'initMonth', request.setup?.world?.initMonth ?? 1),
            },
        },
        snapshot,
        map,
    };
};

const projectWorld = (
    world: InMemoryTurnWorld,
    reservedTurns: InMemoryReservedTurnStore,
    logs: CanonicalTurnSnapshot['logs'],
    messages: CanonicalTurnSnapshot['messages'],
    selector: {
        generalIds: Set<number>;
        cityIds: Set<number>;
        nationIds: Set<number>;
        initialGeneralIds: Set<number>;
        generalCooldowns: GeneralCooldownSelector[];
        nationCooldowns: NationCooldownSelector[];
    }
): CanonicalTurnSnapshot => {
    const state = world.getState();
    const generals = world
        .listGenerals()
        .filter((general) => selector.generalIds.has(general.id))
        .map((general) => ({
            id: general.id,
            name: general.name,
            nationId: general.nationId,
            cityId: general.cityId,
            troopId: general.troopId,
            leadership: general.stats.leadership,
            strength: general.stats.strength,
            intelligence: general.stats.intelligence,
            experience: toDatabaseInt(general.experience),
            dedication: toDatabaseInt(general.dedication),
            expLevel: readNumber(general.meta, 'explevel'),
            officerLevel: general.officerLevel,
            officerCityId: readNumber(
                general.meta,
                'officer_city',
                readNumber(general.meta, 'officerCity', readNumber(general.meta, 'officerCityId'))
            ),
            belong: readNumber(general.meta, 'belong'),
            permission: readString(general.meta, 'permission', 'normal'),
            maxBelong: readNumber(general.meta, 'max_belong'),
            maxDomesticCritical: readNumber(general.meta, 'max_domestic_critical'),
            betray: readNumber(general.meta, 'betray'),
            personality: general.role.personality,
            specialDomestic: general.role.specialDomestic,
            specialWar: general.role.specialWar,
            itemHorse: general.role.items.horse,
            itemWeapon: general.role.items.weapon,
            itemBook: general.role.items.book,
            itemExtra: general.role.items.item,
            injury: general.injury,
            gold: toDatabaseInt(general.gold),
            rice: toDatabaseInt(general.rice),
            crew: toDatabaseInt(general.crew),
            crewTypeId: general.crewTypeId,
            train: toDatabaseInt(general.train),
            atmos: toDatabaseInt(general.atmos),
            age: general.age,
            npcState: general.npcState,
            hasOwner: Boolean(general.userId),
            turnTime: general.turnTime.toISOString(),
            recentWarTime: general.recentWarTime?.toISOString() ?? null,
            lastTurn: general.lastTurn ?? null,
            meta: general.meta,
            leadershipExp: toDatabaseInt(readNumber(general.meta, 'leadership_exp')),
            strengthExp: toDatabaseInt(readNumber(general.meta, 'strength_exp')),
            intelExp: toDatabaseInt(readNumber(general.meta, 'intel_exp')),
            dex1: toDatabaseInt(readNumber(general.meta, 'dex1')),
            dex2: toDatabaseInt(readNumber(general.meta, 'dex2')),
            dex3: toDatabaseInt(readNumber(general.meta, 'dex3')),
            dex4: toDatabaseInt(readNumber(general.meta, 'dex4')),
            dex5: toDatabaseInt(readNumber(general.meta, 'dex5')),
            specAge: toDatabaseInt(readNumber(general.meta, 'specage')),
            specAge2: toDatabaseInt(readNumber(general.meta, 'specage2')),
            inheritActiveActionPoints: readNumber(general.meta, 'inherit_active_action') * 3,
            makeLimit: toDatabaseInt(readNumber(general.meta, 'makelimit')),
            penalty: asRecord(general.penalty),
            killTurn: readNumber(general.meta, 'killturn'),
            mySet: readNumber(general.meta, 'myset'),
        }));
    const nations = world.listNations();
    return {
        schemaVersion: 1,
        engine: 'core2026',
        world: {
            year: state.currentYear,
            month: state.currentMonth,
            tickMinutes: Math.max(1, Math.round(state.tickSeconds / 60)),
            turnTime: state.lastTurnTime.toISOString(),
            isUnited: readNumber(state.meta, 'isUnited'),
            generalCooldowns: selector.generalCooldowns.map(({ generalId, actionName }) => {
                const general = world.getGeneralById(generalId);
                const raw = general?.meta[`next_execute_${actionName}`];
                return {
                    generalId,
                    actionName,
                    nextAvailableTurn: typeof raw === 'number' && Number.isFinite(raw) ? raw : null,
                };
            }),
            nationCooldowns: selector.nationCooldowns.map(({ nationId, actionName }) => {
                const nation = world.getNationById(nationId);
                const raw = nation?.meta[`next_execute_${actionName}`];
                return {
                    nationId,
                    actionName,
                    nextAvailableTurn: typeof raw === 'number' && Number.isFinite(raw) ? raw : null,
                };
            }),
        },
        generals,
        rankData: world
            .listGenerals()
            .filter((general) => selector.generalIds.has(general.id))
            .flatMap((general) =>
                buildLegacyComparableRankRows(general).map((row) =>
                    selector.initialGeneralIds.has(general.id) ? row : { ...row, nationId: 0, value: 0 }
                )
            )
            .map((row) => ({ ...row })),
        cities: world
            .listCities()
            .filter((city) => selector.cityIds.has(city.id))
            .map((city) => ({
                id: city.id,
                name: city.name,
                nationId: city.nationId,
                level: city.level,
                population: toDatabaseInt(city.population),
                populationMax: city.populationMax,
                agriculture: toDatabaseInt(city.agriculture),
                agricultureMax: city.agricultureMax,
                commerce: toDatabaseInt(city.commerce),
                commerceMax: city.commerceMax,
                security: toDatabaseInt(city.security),
                securityMax: city.securityMax,
                supplyState: city.supplyState,
                frontState: city.frontState,
                defence: toDatabaseInt(city.defence),
                defenceMax: city.defenceMax,
                wall: toDatabaseInt(city.wall),
                wallMax: city.wallMax,
                conflict: city.conflict ?? {},
                state: city.state,
                term: readNumber(city.meta, 'term'),
                // The reference snapshot observes MariaDB FLOAT through its text
                // protocol. Project the in-memory binary32 value at that same
                // read boundary before comparing state deltas.
                trust: readLegacyCityTrust(readNumber(city.meta, 'trust')),
                trade: readNumber(city.meta, 'trade'),
                officerSet: readNumber(city.meta, 'officer_set'),
            })),
        nations: nations
            .filter((nation) => selector.nationIds.has(nation.id))
            .map((nation) => ({
                id: nation.id,
                name: nation.name,
                color: nation.color,
                capitalCityId: nation.capitalCityId,
                gold: toDatabaseInt(nation.gold),
                rice: toDatabaseInt(nation.rice),
                tech: readLegacyStoredFloat(readNumber(nation.meta, 'tech')),
                level: nation.level,
                typeCode: nation.typeCode,
                generalCount: world.listGenerals().filter((general) => general.nationId === nation.id).length,
                power: nation.power,
                war: readNumber(nation.meta, 'war'),
                diplomacyLimit: readNumber(nation.meta, 'surlimit'),
                capitalRevision: readNumber(nation.meta, 'capset'),
                strategicCommandLimit: readNumber(nation.meta, 'strategic_cmd_limit'),
                meta: nation.meta,
            })),
        diplomacy: world
            .listDiplomacy()
            .filter((entry) => selector.nationIds.has(entry.fromNationId) && selector.nationIds.has(entry.toNationId))
            .map((entry) => ({ ...entry })),
        generalTurns: generals.flatMap((general) =>
            reservedTurns.getGeneralTurns(Number(general.id)).map((turn, turnIndex) => ({
                generalId: general.id,
                turnIndex,
                action: turn.action,
                args: turn.args,
            }))
        ),
        nationTurns: nations.flatMap((nation) =>
            [12, 11, 10, 9, 8, 7, 6, 5].flatMap((officerLevel) =>
                reservedTurns.getNationTurns(nation.id, officerLevel).map((turn, turnIndex) => ({
                    nationId: nation.id,
                    officerLevel,
                    turnIndex,
                    action: turn.action,
                    args: turn.args,
                }))
            )
        ),
        logs,
        messages,
        watermarks: { logId: logs.length, historyLogId: logs.length, messageId: messages.length },
    };
};

const buildSnapshotReservedTurnDatabaseClient = (snapshot: CanonicalTurnSnapshot) => ({
    generalTurn: {
        findMany: async () =>
            snapshot.generalTurns.map((turn, index) => ({
                id: index + 1,
                generalId: readNumber(turn, 'generalId'),
                turnIdx: readNumber(turn, 'turnIndex'),
                actionCode: readString(turn, 'action', '휴식'),
                arg: asRecord(turn.args),
                createdAt: new Date(0),
            })),
        deleteMany: async () => ({ count: 0 }),
        createMany: async () => ({ count: 0 }),
    },
    nationTurn: {
        findMany: async () =>
            snapshot.nationTurns.map((turn, index) => ({
                id: index + 1,
                nationId: readNumber(turn, 'nationId'),
                officerLevel: readNumber(turn, 'officerLevel'),
                turnIdx: readNumber(turn, 'turnIndex'),
                actionCode: readString(turn, 'action', '휴식'),
                arg: asRecord(turn.args),
                createdAt: new Date(0),
            })),
        deleteMany: async () => ({ count: 0 }),
        createMany: async () => ({ count: 0 }),
    },
});

export const runCoreTurnCommandTrace = async (
    request: TurnCommandFixtureRequest,
    referenceBefore: CanonicalTurnSnapshot
): Promise<CanonicalTurnCommandTrace> => {
    const unitSet = await loadUnitSetDefinitionByName('che');
    const map = await loadMapDefinitionByName('che');
    const worldInput = buildCoreTurnCommandWorldInput(request, referenceBefore, unitSet, map);
    const { state, snapshot } = worldInput;
    const selector = {
        generalIds: new Set([
            ...referenceBefore.generals.map((row) => readNumber(row, 'id')),
            ...(request.observe?.generalIds ?? []),
        ]),
        cityIds: new Set(referenceBefore.cities.map((row) => readNumber(row, 'id'))),
        nationIds: new Set([
            ...referenceBefore.nations.map((row) => readNumber(row, 'id')),
            ...(request.observe?.nationIds ?? []),
        ]),
        initialGeneralIds: new Set(referenceBefore.generals.map((row) => readNumber(row, 'id'))),
        generalCooldowns: request.observe?.generalCooldowns ?? [],
        nationCooldowns: request.observe?.nationCooldowns ?? [],
    };
    const reservedTurns = new InMemoryReservedTurnStore(
        buildSnapshotReservedTurnDatabaseClient(referenceBefore) as never,
        {
            maxGeneralTurns: 30,
            maxNationTurns: 12,
        }
    );
    await reservedTurns.loadAll();
    const actor = snapshot.generals.find((general) => general.id === request.actorGeneralId);
    if (!actor) {
        throw new Error(`Missing actor general ${request.actorGeneralId}`);
    }
    const args = resolveCoreTurnCommandArgs(request);
    if (request.kind === 'general') {
        reservedTurns.getGeneralTurns(actor.id)[0] = { action: request.action, args };
    } else {
        reservedTurns.getNationTurns(actor.nationId, actor.officerLevel)[0] = {
            action: request.action,
            args,
        };
    }

    let world: InMemoryTurnWorld | null = null;
    let resolution:
        | {
              kind: 'nation' | 'general';
              actionKey: string;
              requestedAction: string;
              usedFallback: boolean;
              blockedReason?: string;
          }
        | undefined;
    const commandRngCalls: RandomCall[] = [];
    const handler = await createReservedTurnHandler({
        reservedTurns,
        scenarioConfig: snapshot.scenarioConfig,
        scenarioMeta: snapshot.scenarioMeta,
        map,
        unitSet,
        getWorld: () => world,
        commandProfile: createCoreTurnCommandProfile(request),
        commandRngFactory: ({ kind, actionKey, seed }) => {
            const tracing = new TracingRng(new LiteHashDRBG(seed));
            if (kind === request.kind && actionKey === request.action) {
                commandRngCalls.push(...tracing.calls);
                return new RandUtil({
                    getMaxInt: () => tracing.getMaxInt(),
                    nextBytes: (bytes) => {
                        const result = tracing.nextBytes(bytes);
                        commandRngCalls.splice(0, commandRngCalls.length, ...tracing.calls);
                        return result;
                    },
                    nextBits: (bits) => {
                        const result = tracing.nextBits(bits);
                        commandRngCalls.splice(0, commandRngCalls.length, ...tracing.calls);
                        return result;
                    },
                    nextInt: (max) => {
                        const result = tracing.nextInt(max);
                        commandRngCalls.splice(0, commandRngCalls.length, ...tracing.calls);
                        return result;
                    },
                    nextFloat1: () => {
                        const result = tracing.nextFloat1();
                        commandRngCalls.splice(0, commandRngCalls.length, ...tracing.calls);
                        return result;
                    },
                });
            }
            return new RandUtil(new LiteHashDRBG(seed));
        },
        onActionResolved: (payload) => {
            if (payload.kind === request.kind && payload.requestedAction === request.action) {
                resolution = payload;
            }
        },
    });
    world = new InMemoryTurnWorld(state, snapshot, {
        schedule: {
            entries: [{ startMinute: 0, tickMinutes: Math.max(1, Math.round(state.tickSeconds / 60)) }],
        },
        generalTurnHandler: handler,
    });
    const before = projectWorld(world, reservedTurns, [], [], selector);
    world.executeGeneralTurn(actor);
    const dirty = world.peekDirtyState();
    const after = projectWorld(
        world,
        reservedTurns,
        dirty.logs.map((log, index) => ({
            id: index + 1,
            scope: log.scope,
            category: log.category,
            // Keep the product draft unchanged. Inferring an owner here hid
            // GENERAL logs that finalizeLogEntry would reject in production.
            generalId: log.generalId,
            nationId: log.nationId,
            year: state.currentYear,
            month: state.currentMonth,
            text: log.text,
        })),
        dirty.messages.map((message, index) => ({
            id: index + 1,
            payload: message,
        })),
        selector
    );

    return {
        schemaVersion: 1,
        engine: 'core2026',
        execution: {
            kind: request.kind,
            actorGeneralId: request.actorGeneralId,
            action: request.action,
            args,
            seedDomain: request.kind === 'general' ? 'generalCommand' : 'nationCommand',
            outcome: resolution,
        },
        before,
        after,
        rng: commandRngCalls,
    };
};
