import type {
    City,
    Constraint,
    ConstraintContext,
    ConstraintResult,
    General,
    GeneralItemSlots,
    GeneralActionDefinition,
    GeneralTurnCommandSpec,
    GeneralTurnCommandKey,
    MapDefinition,
    Nation,
    NationTurnCommandKey,
    NationTurnCommandSpec,
    RequirementKey,
    StateView,
    TurnCommandEnv,
    TriggerValue,
    UnitSetDefinition,
} from '@sammo-ts/logic';
import { evaluateConstraints } from '@sammo-ts/logic';
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
import { CommandResolver as RecruitmentCommandResolver } from '@sammo-ts/logic/actions/turn/general/che_징병.js';
import { projectItemSlots, readItemInventoryFromMeta } from '@sammo-ts/logic/items/index.js';
import { getTechAbility, getTechLevel, isCrewTypeAvailable } from '@sammo-ts/logic/world/unitSet.js';
import { asRecord, isRecord } from '@sammo-ts/common';

import type { CityRow, GeneralRow, NationRow, WorldStateRow } from '../context.js';
import {
    buildTurnCommandInputFields,
    loadTurnCommandSpecs,
    type TurnCommandInputField,
    type TurnCommandInputOptions,
    type TurnCommandRecruitmentInfo,
} from './commandInput.js';

type AvailabilityStatus = 'available' | 'blocked' | 'needsInput' | 'unknown';

export interface TurnCommandAvailability {
    key: string;
    name: string;
    reqArg: boolean;
    possible: boolean;
    status: AvailabilityStatus;
    reason?: string;
    inputFields: TurnCommandInputField[];
}

export interface TurnCommandGroup {
    category: string;
    values: TurnCommandAvailability[];
}

export interface TurnCommandTable {
    general: TurnCommandGroup[];
    nation: TurnCommandGroup[];
    inputOptions: TurnCommandInputOptions;
}

type CommandEnv = TurnCommandEnv;

interface AvailabilityCore {
    possible: boolean;
    status: AvailabilityStatus;
    reason?: string;
}

interface CommandEntry {
    category: string;
    definition: GeneralActionDefinition;
    reqArg: boolean;
    availabilityArgs: Readonly<Record<string, unknown>>;
    inputFields: TurnCommandInputField[];
    evaluate?: (ctx: ConstraintContext, view: StateView) => AvailabilityCore;
}

const REF_GENERAL_COMMAND_GROUPS = [
    {
        category: '개인',
        commands: [
            '휴식',
            'che_요양',
            'che_단련',
            'che_숙련전환',
            'che_견문',
            'che_은퇴',
            'che_장비매매',
            'che_군량매매',
            'che_내정특기초기화',
            'che_전투특기초기화',
        ],
    },
    {
        category: '내정',
        commands: [
            'che_농지개간',
            'che_상업투자',
            'che_기술연구',
            'che_수비강화',
            'che_성벽보수',
            'che_치안강화',
            'che_정착장려',
            'che_주민선정',
        ],
    },
    {
        category: '군사',
        commands: [
            'che_징병',
            'che_모병',
            'che_훈련',
            'che_사기진작',
            'che_출병',
            'che_집합',
            'che_소집해제',
            'che_첩보',
        ],
    },
    {
        category: '인사',
        commands: [
            'che_이동',
            'che_강행',
            'che_인재탐색',
            'che_등용',
            'che_귀환',
            'che_임관',
            'che_랜덤임관',
            'che_장수대상임관',
        ],
    },
    {
        category: '계략',
        commands: ['che_선동', 'che_탈취', 'che_파괴', 'che_화계'],
    },
    {
        category: '국가',
        commands: ['che_증여', 'che_헌납', 'che_물자조달', 'che_하야', 'che_거병', 'che_건국', 'che_선양', 'che_해산'],
    },
] as const satisfies ReadonlyArray<{
    category: string;
    commands: ReadonlyArray<GeneralTurnCommandKey>;
}>;

const REF_GENERAL_CATEGORY_ORDER = new Map<string, number>(
    REF_GENERAL_COMMAND_GROUPS.map(({ category }, index) => [category, index] as const)
);
const REF_GENERAL_COMMAND_POSITION = new Map<string, { category: string; index: number }>(
    REF_GENERAL_COMMAND_GROUPS.flatMap(({ category, commands }) =>
        commands.map((command, index) => [command, { category, index }] as const)
    )
);

const REF_NATION_COMMAND_GROUPS = [
    {
        category: '휴식',
        commands: ['휴식'],
    },
    {
        category: '인사',
        commands: ['che_발령', 'che_포상', 'che_몰수', 'che_부대탈퇴지시'],
    },
    {
        category: '외교',
        commands: ['che_물자원조', 'che_불가침제의', 'che_선전포고', 'che_종전제의', 'che_불가침파기제의'],
    },
    {
        category: '특수',
        commands: ['che_초토화', 'che_천도', 'che_증축', 'che_감축'],
    },
    {
        category: '전략',
        commands: [
            'che_필사즉생',
            'che_백성동원',
            'che_수몰',
            'che_허보',
            'che_의병모집',
            'che_이호경식',
            'che_급습',
            'che_피장파장',
        ],
    },
    {
        category: '기타',
        commands: ['che_국기변경', 'che_국호변경'],
    },
] as const satisfies ReadonlyArray<{
    category: string;
    commands: ReadonlyArray<NationTurnCommandKey>;
}>;

const REF_NATION_CATEGORY_ORDER = new Map<string, number>(
    REF_NATION_COMMAND_GROUPS.map(({ category }, index) => [category, index] as const)
);
const REF_NATION_COMMAND_POSITION = new Map<string, { category: string; index: number }>(
    REF_NATION_COMMAND_GROUPS.flatMap(({ category, commands }) =>
        commands.map((command, index) => [command, { category, index }] as const)
    )
);

const INPUT_REQUIREMENT_KINDS = new Set<RequirementKey['kind']>([
    'destGeneral',
    'destCity',
    'destNation',
    'diplomacy',
    'arg',
]);

const AVAILABILITY_PRIORITY: Record<AvailabilityStatus, number> = {
    available: 3,
    needsInput: 2,
    unknown: 1,
    blocked: 0,
};

const DEFAULT_GENERAL_GOLD = 1000;
const DEFAULT_GENERAL_RICE = 1000;
const DEFAULT_CREW_TYPE_ID = 1100;

const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
    isRecord(value) ? (value as Record<string, TriggerValue>) : {};

const readMetaNumber = (meta: Record<string, TriggerValue>, key: string): number | null => {
    const value = meta[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 null;
};

const ensureGeneralMeta = (meta: Record<string, TriggerValue>, generalId: number): General['meta'] => {
    const killturn = readMetaNumber(meta, 'killturn');
    if (killturn === null) {
        throw new Error(`general.meta.killturn is required (generalId=${generalId}).`);
    }
    return { ...meta, killturn } as General['meta'];
};

const normalizeCode = (value: string | null | undefined): string | null => {
    if (!value || value === 'None') {
        return null;
    }
    return value;
};

class MemoryStateView implements StateView {
    private readonly store = new Map<string, unknown>();

    has(req: RequirementKey): boolean {
        return this.store.has(this.getKey(req));
    }

    get(req: RequirementKey): unknown | null {
        return this.store.get(this.getKey(req)) ?? null;
    }

    set(req: RequirementKey, value: unknown): void {
        this.store.set(this.getKey(req), value);
    }

    private getKey(req: RequirementKey): string {
        switch (req.kind) {
            case 'general':
                return `general:${req.id}`;
            case 'generalList':
                return 'general:list';
            case 'city':
                return `city:${req.id}`;
            case 'nation':
                return `nation:${req.id}`;
            case 'destGeneral':
                return `destGeneral:${req.id}`;
            case 'destCity':
                return `destCity:${req.id}`;
            case 'destNation':
                return `destNation:${req.id}`;
            case 'diplomacy':
                return `diplomacy:${req.srcNationId}:${req.destNationId}`;
            case 'diplomacyList':
                return 'diplomacy:list';
            case 'arg':
                return `arg:${req.key}`;
            case 'env':
                return `env:${req.key}`;
            default:
                return 'unknown';
        }
    }
}

const resolveNumber = (source: Record<string, unknown>, keys: string[], fallback: number): number => {
    for (const key of keys) {
        const value = source[key];
        if (typeof value === 'number' && Number.isFinite(value)) {
            return value;
        }
    }
    return fallback;
};

const resolveOptionalString = (source: Record<string, unknown>, keys: string[]): string | null => {
    for (const key of keys) {
        const value = source[key];
        if (typeof value === 'string') {
            return normalizeCode(value);
        }
    }
    return null;
};

const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
    const config = asRecord(worldState.config);
    const constValues = asRecord(config.const);

    return {
        develCost: resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0),
        trainDelta: resolveNumber(constValues, ['trainDelta'], 0),
        atmosDelta: resolveNumber(constValues, ['atmosDelta'], 0),
        trainSideEffectByAtmosTurn: resolveNumber(constValues, ['trainSideEffectByAtmosTurn'], 1),
        maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], 0),
        maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], 0),
        sabotageDefaultProb: resolveNumber(constValues, ['sabotageDefaultProb'], 0),
        sabotageProbCoefByStat: resolveNumber(constValues, ['sabotageProbCoefByStat'], 0),
        sabotageDefenceCoefByGeneralCount: resolveNumber(constValues, ['sabotageDefenceCoefByGeneralCount'], 0),
        sabotageDamageMin: resolveNumber(constValues, ['sabotageDamageMin'], 0),
        sabotageDamageMax: resolveNumber(constValues, ['sabotageDamageMax'], 0),
        openingPartYear: resolveNumber(constValues, ['openingPartYear'], 0),
        maxGeneral: resolveNumber(constValues, ['defaultMaxGeneral', 'maxGeneral'], 0),
        defaultNpcGold: resolveNumber(constValues, ['defaultNpcGold', 'defaultGold'], DEFAULT_GENERAL_GOLD),
        defaultNpcRice: resolveNumber(constValues, ['defaultNpcRice', 'defaultRice'], DEFAULT_GENERAL_RICE),
        defaultCrewTypeId: resolveNumber(constValues, ['defaultCrewTypeId'], DEFAULT_CREW_TYPE_ID),
        defaultSpecialDomestic: resolveOptionalString(constValues, ['defaultSpecialDomestic']),
        defaultSpecialWar: resolveOptionalString(constValues, ['defaultSpecialWar']),
        initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], 0),
        maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 12),
        maxStatLevel: resolveNumber(constValues, ['maxLevel'], 255),
        techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], 5),
        initialAllowedTechLevel: resolveNumber(constValues, ['initialAllowedTechLevel'], 1),
        baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
        baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
        generalMinimumGold: resolveNumber(constValues, ['generalMinimumGold'], 0),
        generalMinimumRice: resolveNumber(constValues, ['generalMinimumRice'], 500),
        npcSeizureMessageProb: resolveNumber(constValues, ['npcSeizureMessageProb'], 0.01),
        maxResourceActionAmount: resolveNumber(constValues, ['maxResourceActionAmount'], 0),
    };
};

const buildConstraintEnv = (worldState: WorldStateRow): Record<string, unknown> => {
    const config = asRecord(worldState.config);
    const constValues = asRecord(config.const);
    const meta = asRecord(worldState.meta);
    const scenarioMeta = asRecord(meta.scenarioMeta);
    const startYear = typeof scenarioMeta.startYear === 'number' ? scenarioMeta.startYear : undefined;
    const relYear = typeof startYear === 'number' ? worldState.currentYear - startYear : undefined;
    const joinModeRaw = config.join_mode ?? config.joinMode;
    const joinMode = typeof joinModeRaw === 'string' ? joinModeRaw : 'full';

    return {
        currentYear: worldState.currentYear,
        currentMonth: worldState.currentMonth,
        year: worldState.currentYear,
        month: worldState.currentMonth,
        startYear,
        relYear,
        join_mode: joinMode,
        openingPartYear: resolveNumber(constValues, ['openingPartYear'], 0),
    };
};

const mapGeneralRow = (row: GeneralRow, missingKillturnFallback?: number): General => {
    const legacySlots: GeneralItemSlots = {
        horse: normalizeCode(row.horseCode),
        weapon: normalizeCode(row.weaponCode),
        book: normalizeCode(row.bookCode),
        item: normalizeCode(row.itemCode),
    };
    const rawMeta = asTriggerRecord(row.meta);
    const meta =
        readMetaNumber(rawMeta, 'killturn') === null && missingKillturnFallback !== undefined
            ? ({ ...rawMeta, killturn: missingKillturnFallback } as General['meta'])
            : ensureGeneralMeta(rawMeta, row.id);
    const itemInventory = readItemInventoryFromMeta(meta, legacySlots);
    return {
        id: row.id,
        name: row.name,
        nationId: row.nationId,
        cityId: row.cityId,
        troopId: row.troopId,
        stats: {
            leadership: row.leadership,
            strength: row.strength,
            intelligence: row.intel,
        },
        experience: row.experience,
        dedication: row.dedication,
        officerLevel: row.officerLevel,
        role: {
            personality: normalizeCode(row.personalCode),
            specialDomestic: normalizeCode(row.specialCode),
            specialWar: normalizeCode(row.special2Code),
            items: projectItemSlots(itemInventory),
        },
        injury: row.injury,
        gold: row.gold,
        rice: row.rice,
        crew: row.crew,
        crewTypeId: row.crewTypeId,
        train: row.train,
        atmos: row.atmos,
        age: row.age,
        npcState: row.npcState,
        triggerState: {
            flags: {},
            counters: {},
            modifiers: {},
            meta: {},
        },
        itemInventory,
        meta,
    };
};

const mapCityRow = (row: CityRow): City => {
    const meta = asTriggerRecord(row.meta);
    const state = typeof meta.state === 'number' && Number.isFinite(meta.state) ? Math.floor(meta.state) : 0;
    return {
        id: row.id,
        name: row.name,
        nationId: row.nationId,
        level: row.level,
        state,
        population: row.population,
        populationMax: row.populationMax,
        agriculture: row.agriculture,
        agricultureMax: row.agricultureMax,
        commerce: row.commerce,
        commerceMax: row.commerceMax,
        security: row.security,
        securityMax: row.securityMax,
        supplyState: row.supplyState,
        frontState: row.frontState,
        defence: row.defence,
        defenceMax: row.defenceMax,
        wall: row.wall,
        wallMax: row.wallMax,
        meta: {
            ...meta,
            trust: row.trust,
            ...(row.trade === null ? {} : { trade: row.trade }),
            region: row.region,
        },
    };
};

const mapNationRow = (row: NationRow): Nation => ({
    id: row.id,
    name: row.name,
    color: row.color,
    capitalCityId: row.capitalCityId,
    chiefGeneralId: null,
    gold: row.gold,
    rice: row.rice,
    power: 0,
    level: row.level,
    typeCode: row.typeCode,
    meta: {
        ...asTriggerRecord(row.meta),
        tech: row.tech,
    },
});

export const buildRecruitmentCommandInfo = (options: {
    worldState: WorldStateRow;
    general: GeneralRow;
    city: CityRow | null;
    nation: NationRow | null;
    cities: CityRow[];
    map: MapDefinition;
    unitSet: UnitSetDefinition;
    generalActionModules?: ReadonlyArray<GeneralActionModule | null | undefined>;
}): TurnCommandRecruitmentInfo => {
    const general = mapGeneralRow(options.general);
    const city = options.city ? mapCityRow(options.city) : undefined;
    const nation = options.nation ? mapNationRow(options.nation) : null;
    const cities = options.cities.map(mapCityRow);
    const context = city ? { general, city, nation } : { general, nation };
    const command = new RecruitmentCommandResolver(options.generalActionModules ?? [], {});
    const tech = options.nation?.tech ?? 0;
    const techAbility = getTechAbility(tech);
    const constraintEnv = buildConstraintEnv(options.worldState);
    const startYear = typeof constraintEnv.startYear === 'number' ? constraintEnv.startYear : undefined;
    const availabilityContext = {
        general,
        nation,
        map: options.map,
        cities,
        currentYear: options.worldState.currentYear,
        ...(startYear === undefined ? {} : { startYear }),
    };
    const crewTypes = options.unitSet.crewTypes ?? [];
    const armTypes = Object.entries(options.unitSet.armTypes ?? {})
        .map(([armType, armName]) => ({ armType: Number(armType), armName }))
        .filter((entry) => Number.isFinite(entry.armType))
        .sort((left, right) => left.armType - right.armType);

    const groups = armTypes.map(({ armType, armName }) => ({
        armType,
        armName,
        values: crewTypes
            .filter((crewType) => crewType.armType === armType)
            .map((crewType) => {
                const displayCost = command.getDisplayUnitCost(context, crewType);
                const requiredTech = crewType.requirements.find((requirement) => requirement.type === 'ReqTech');
                return {
                    id: crewType.id,
                    armType,
                    name: crewType.name,
                    available: isCrewTypeAvailable(options.unitSet, crewType.id, availabilityContext),
                    special:
                        requiredTech?.type === 'ReqTech' &&
                        typeof requiredTech.tech === 'number' &&
                        requiredTech.tech > 0,
                    attack: crewType.attack + techAbility,
                    defence: crewType.defence + techAbility,
                    speed: crewType.speed,
                    avoid: crewType.avoid,
                    baseCost: displayCost.gold,
                    baseRice: displayCost.rice,
                    info: [...crewType.info],
                };
            }),
    }));
    const currentCrewTypeName = crewTypes.find((crewType) => crewType.id === general.crewTypeId)?.name ?? '-';

    return {
        techLevel: getTechLevel(tech),
        leadership: command.resolveLeadership(context),
        fullLeadership: command.resolveFullLeadership(context),
        currentCrewTypeId: general.crewTypeId,
        currentCrewTypeName,
        crew: general.crew,
        gold: general.gold,
        groups,
    };
};

const buildStateView = (
    general: General,
    city: City | null,
    nation: Nation | null,
    generalList: General[] | null,
    env: Record<string, unknown>
): StateView => {
    const view = new MemoryStateView();
    view.set({ kind: 'general', id: general.id }, general);
    for (const [key, value] of Object.entries(env)) {
        if (value !== undefined) {
            view.set({ kind: 'env', key }, value);
        }
    }
    if (city) {
        view.set({ kind: 'city', id: city.id }, city);
    }
    if (nation) {
        view.set({ kind: 'nation', id: nation.id }, nation);
    }
    if (generalList) {
        view.set({ kind: 'generalList' }, generalList);
    }
    return view;
};

const evaluateAvailability = (
    constraints: Constraint[],
    ctx: ConstraintContext,
    view: StateView,
    reqArg: boolean
): AvailabilityCore => {
    const result = evaluateConstraints(constraints, ctx, view);
    if (result.kind === 'allow') {
        return { possible: true, status: 'available' };
    }
    if (result.kind === 'deny') {
        return {
            possible: false,
            status: 'blocked',
            reason: result.reason,
        };
    }
    const missingKinds = new Set(result.missing.map((req: RequirementKey) => req.kind));
    const inputOnlyMissing =
        missingKinds.size === 0 ? reqArg : Array.from(missingKinds).every((kind) => INPUT_REQUIREMENT_KINDS.has(kind));
    if (inputOnlyMissing) {
        return {
            possible: true,
            status: 'needsInput',
            reason: '대상 선택 필요',
        };
    }
    return {
        possible: false,
        status: 'unknown',
        reason: '정보 부족',
    };
};

const evaluateDefinition = (
    definition: GeneralActionDefinition,
    ctx: ConstraintContext,
    view: StateView,
    reqArg: boolean,
    args: unknown
): AvailabilityCore => {
    const constraints =
        ctx.mode === 'precheck' && definition.buildMinConstraints
            ? definition.buildMinConstraints(ctx, args)
            : definition.buildConstraints(ctx, args);
    return evaluateAvailability(constraints, ctx, view, reqArg);
};

const pickAvailability = (lhs: AvailabilityCore, rhs: AvailabilityCore): AvailabilityCore =>
    AVAILABILITY_PRIORITY[lhs.status] >= AVAILABILITY_PRIORITY[rhs.status] ? lhs : rhs;

type TurnCommandSpec = GeneralTurnCommandSpec | NationTurnCommandSpec;

const buildEntries = (env: CommandEnv, specs: TurnCommandSpec[]): CommandEntry[] => {
    const entries: CommandEntry[] = [];

    for (const spec of specs) {
        const definition = spec.createDefinition(env);
        const entry: CommandEntry = {
            category: spec.category,
            definition,
            reqArg: spec.reqArg,
            availabilityArgs: spec.reqArg ? spec.availabilityArgs : {},
            inputFields: buildTurnCommandInputFields(spec),
        };

        if (spec.key === 'che_포상') {
            entry.evaluate = (ctx, view) => {
                const gold = evaluateDefinition(definition, ctx, view, true, {
                    isGold: true,
                    amount: 1,
                    destGeneralId: 0,
                });
                const rice = evaluateDefinition(definition, ctx, view, true, {
                    isGold: false,
                    amount: 1,
                    destGeneralId: 0,
                });
                return pickAvailability(gold, rice);
            };
        }

        entries.push(entry);
    }

    return entries;
};

const buildGroups = (entries: CommandEntry[], ctx: ConstraintContext, view: StateView): TurnCommandGroup[] => {
    const groups = new Map<string, TurnCommandAvailability[]>();

    for (const entry of entries) {
        const availability = entry.evaluate
            ? entry.evaluate(ctx, view)
            : evaluateDefinition(entry.definition, ctx, view, entry.reqArg, entry.availabilityArgs);
        const value: TurnCommandAvailability = {
            key: entry.definition.key,
            name: entry.definition.name,
            reqArg: entry.reqArg,
            inputFields: entry.inputFields,
            ...availability,
        };

        const list = groups.get(entry.category);
        if (list) {
            list.push(value);
        } else {
            groups.set(entry.category, [value]);
        }
    }

    return Array.from(groups.entries()).map(([category, values]) => ({
        category,
        values,
    }));
};

const projectRefGeneralCommandGroups = (entries: CommandEntry[]): CommandEntry[] =>
    entries
        .map((entry, profileIndex) => {
            const refPosition = REF_GENERAL_COMMAND_POSITION.get(entry.definition.key as GeneralTurnCommandKey);
            return {
                entry: refPosition ? { ...entry, category: refPosition.category } : entry,
                categoryIndex:
                    REF_GENERAL_CATEGORY_ORDER.get(refPosition?.category ?? entry.category) ?? Number.MAX_SAFE_INTEGER,
                commandIndex: refPosition?.index ?? profileIndex,
                profileIndex,
            };
        })
        .sort(
            (left, right) =>
                left.categoryIndex - right.categoryIndex ||
                left.commandIndex - right.commandIndex ||
                left.profileIndex - right.profileIndex
        )
        .map(({ entry }) => entry);

const projectRefNationCommandGroups = (entries: CommandEntry[]): CommandEntry[] =>
    entries
        .map((entry, profileIndex) => {
            const refPosition = REF_NATION_COMMAND_POSITION.get(entry.definition.key as NationTurnCommandKey);
            return {
                entry: refPosition ? { ...entry, category: refPosition.category } : entry,
                categoryIndex:
                    REF_NATION_CATEGORY_ORDER.get(refPosition?.category ?? entry.category) ?? Number.MAX_SAFE_INTEGER,
                commandIndex: refPosition?.index ?? profileIndex,
                profileIndex,
            };
        })
        .sort(
            (left, right) =>
                left.categoryIndex - right.categoryIndex ||
                left.commandIndex - right.commandIndex ||
                left.profileIndex - right.profileIndex
        )
        .map(({ entry }) => entry);

export const buildTurnCommandTable = async (options: {
    worldState: WorldStateRow;
    general: GeneralRow;
    city: CityRow | null;
    nation: NationRow | null;
    nationGenerals: GeneralRow[] | null;
    inputOptions?: TurnCommandInputOptions;
}): Promise<TurnCommandTable> => {
    // 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
    const general = mapGeneralRow(options.general);
    const city = options.city ? mapCityRow(options.city) : null;
    const nation = options.nation ? mapNationRow(options.nation) : null;
    const generalList = options.nationGenerals ? options.nationGenerals.map(mapGeneralRow) : null;
    const constraintEnv = buildConstraintEnv(options.worldState);
    const view = buildStateView(general, city, nation, generalList, constraintEnv);

    const ctx: ConstraintContext = {
        actorId: general.id,
        cityId: city?.id,
        nationId: nation?.id,
        args: {},
        env: constraintEnv,
        mode: 'precheck',
    };

    const env = buildCommandEnv(options.worldState);
    const { general: generalSpecs, nation: nationSpecs } = await loadTurnCommandSpecs();
    const generalEntries = buildEntries(env, generalSpecs);
    const nationEntries = buildEntries(env, nationSpecs);

    return {
        general: buildGroups(projectRefGeneralCommandGroups(generalEntries), ctx, view),
        nation: buildGroups(projectRefNationCommandGroups(nationEntries), ctx, view),
        inputOptions: options.inputOptions ?? {
            cities: [],
            nations: [],
            nationTargets: {},
            generals: [],
            generalTargets: {},
            crewTypes: [],
            armTypes: [],
            nationTypes: [],
            colors: [],
            items: {},
            recruitment: null,
            amountPresets: {},
        },
    };
};

export const evaluateReservedTurnPermission = async (options: {
    worldState: WorldStateRow;
    general: GeneralRow;
    scope: 'general' | 'nation';
    action: string;
    args: Record<string, unknown>;
}): Promise<ConstraintResult> => {
    const specs = await loadTurnCommandSpecs();
    const spec = specs[options.scope].find((entry) => entry.key === options.action);
    if (!spec) {
        throw new Error(`Unknown ${options.scope} turn command: ${options.action}`);
    }

    const definition = spec.createDefinition(buildCommandEnv(options.worldState));
    if (!definition.buildPermissionConstraints) {
        return { kind: 'allow' };
    }

    const general = mapGeneralRow(options.general, 0);
    const env = buildConstraintEnv(options.worldState);
    const ctx: ConstraintContext = {
        actorId: general.id,
        cityId: general.cityId > 0 ? general.cityId : undefined,
        nationId: general.nationId > 0 ? general.nationId : undefined,
        args: options.args,
        env,
        mode: 'full',
    };
    const view = buildStateView(general, null, null, null, env);
    return evaluateConstraints(definition.buildPermissionConstraints(ctx, options.args), ctx, view);
};
