import { describe, expect, it } from 'vitest';
import type {
    ConstraintContext,
    LogEntryDraft,
    RequirementKey,
    StateView,
    TurnSchedule,
    UnitSetDefinition,
} from '@sammo-ts/logic';
import { DEFAULT_TURN_COMMAND_PROFILE, LogCategory, evaluateConstraints } from '@sammo-ts/logic';
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
import type { GeneralAiDebugState } from '../src/turn/ai/generalAi.js';
import type { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { GeneralAI } from '../src/turn/ai/generalAi.js';
import { do징병 } from '../src/turn/ai/generalAi/general/recruitActions.js';
import { buildCommandEnv, buildReservedTurnDefinitions } from '../src/turn/reservedTurnCommands.js';
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
import { round } from 'es-toolkit';
import { createTurnTestHarness, createWorldDebugger } from './helpers/turnTestHarness.js';

const mockDate = new Date('0179-08-01T00:00:00Z');

const createNpcGeneral = (
    id: number,
    cityId: number,
    stats: { leadership: number; strength: number; intelligence: number },
    npcState: number
): TurnGeneral => ({
    id,
    name: `NPC_${id}`,
    nationId: 0,
    cityId,
    troopId: 0,
    stats,
    turnTime: mockDate,
    role: {
        items: { horse: null, weapon: null, book: null, item: null },
        personality: null,
        specialDomestic: null,
        specialWar: null,
    },
    triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
    meta: { killturn: 800 },
    officerLevel: 1,
    experience: 0,
    dedication: 0,
    injury: 0,
    gold: 5000,
    rice: 5000,
    crew: 0,
    crewTypeId: 0,
    train: 0,
    atmos: 0,
    age: 30,
    npcState,
});

describe('NPC 대형 시뮬레이션', () => {
    it('NPC 국가 성장 기준을 만족해야 한다', async () => {
        const smallMediumCityIds = LARGE_TEST_MAP.cities
            .filter((city) => [4, 5].includes(city.level))
            .map((city) => city.id);

        const npcPerType = smallMediumCityIds.length * 20;
        const generals: TurnGeneral[] = [];

        for (let i = 0; i < npcPerType; i += 1) {
            const cityId = smallMediumCityIds[i % smallMediumCityIds.length];
            generals.push(
                createNpcGeneral(generals.length + 1, cityId, { leadership: 75, strength: 75, intelligence: 10 }, 2)
            );
        }

        for (let i = 0; i < npcPerType; i += 1) {
            const cityId = smallMediumCityIds[i % smallMediumCityIds.length];
            generals.push(
                createNpcGeneral(generals.length + 1, cityId, { leadership: 75, strength: 10, intelligence: 75 }, 2)
            );
        }

        const cities = buildLargeTestCities();
        const initialCityStats = new Map(
            cities.map((city) => [
                city.id,
                {
                    population: city.population,
                    agriculture: city.agriculture,
                    commerce: city.commerce,
                    security: city.security,
                    defence: city.defence,
                    wall: city.wall,
                },
            ])
        );

        const unitSet: UnitSetDefinition = {
            id: 'test_unit_set',
            name: 'TestUnitSet',
            defaultCrewTypeId: 1100,
            crewTypes: [
                {
                    id: 1100,
                    armType: 1,
                    name: '보병',
                    attack: 10,
                    defence: 10,
                    speed: 10,
                    avoid: 0,
                    magicCoef: 0,
                    cost: 10,
                    rice: 1,
                    requirements: [],
                    attackCoef: {},
                    defenceCoef: {},
                    info: [],
                    initSkillTrigger: null,
                    phaseSkillTrigger: null,
                    iActionList: null,
                },
            ],
        };

        const snapshot: TurnWorldSnapshot = {
            generals: generals as any,
            cities: cities as any,
            nations: [],
            troops: [],
            diplomacy: [],
            events: [],
            initialEvents: [],
            map: LARGE_TEST_MAP as any,
            scenarioConfig: {
                stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
                iconPath: '',
                map: {},
                const: {
                    openingPartYear: 3,
                    develCost: 10,
                    baseGold: 1000,
                    baseRice: 1000,
                    maxResourceActionAmount: 10000,
                    minAvailableRecruitPop: 0,
                },
                environment: { mapName: 'large_test_map', unitSet: 'default' },
            },
            scenarioMeta: {
                startYear: 180,
            } as any,
            unitSet: unitSet as any,
        };

        const state: TurnWorldState = {
            id: 1,
            currentYear: 179,
            currentMonth: 8,
            tickSeconds: 600,
            lastTurnTime: mockDate,
            meta: { seed: 1, initYear: 179, initMonth: 8 },
        };

        const schedule: TurnSchedule = {
            entries: [{ startMinute: 0, tickMinutes: 10 }],
        };

        const worldRef = { current: null as InMemoryTurnWorld | null };

        type TurnTrace = {
            year: number;
            month: number;
            nationId: number;
            generalId: number;
            gold: number;
            rice: number;
            crew: number;
            train: number;
            atmos: number;
            actionText: string;
            actionKey: string;
            requestedAction: string;
            usedFallback: boolean;
            blockedReason?: string;
            ok: boolean;
            error?: unknown;
            logs: LogEntryDraft[];
            aiState?: GeneralAiDebugState;
        };

        const turnTraces: TurnTrace[] = [];
        const traceByGeneralId = new Map<number, TurnTrace>();

        const commandEnv = buildCommandEnv(snapshot.scenarioConfig, snapshot.unitSet);

        const { world, reservedTurnStore, runUntil } = await createTurnTestHarness({
            snapshot,
            state,
            schedule,
            map: LARGE_TEST_MAP,
            worldRef,
            onActionResolved: (payload) => {
                if (payload.kind !== 'general') {
                    return;
                }
                const trace = traceByGeneralId.get(payload.generalId);
                if (!trace) {
                    return;
                }
                trace.actionKey = payload.actionKey;
                trace.requestedAction = payload.requestedAction;
                trace.usedFallback = payload.usedFallback;
                trace.blockedReason = payload.blockedReason;
                trace.aiState = payload.aiState;
            },
            wrapGeneralTurnHandler: (handler) => ({
                execute: (ctx) => {
                    const trace: TurnTrace = {
                        year: ctx.world.currentYear,
                        month: ctx.world.currentMonth,
                        nationId: ctx.general.nationId,
                        generalId: ctx.general.id,
                        gold: ctx.general.gold,
                        rice: ctx.general.rice,
                        crew: ctx.general.crew,
                        train: ctx.general.train,
                        atmos: ctx.general.atmos,
                        actionKey: 'unknown',
                        requestedAction: 'unknown',
                        usedFallback: false,
                        ok: true,
                        actionText: 'unknown',
                        logs: [],
                    };
                    traceByGeneralId.set(ctx.general.id, trace);
                    const result = handler.execute(ctx);
                    const actionLog = result.logs?.find((log) => log.category === LogCategory.ACTION);
                    trace.actionText = actionLog?.text ?? 'unknown';
                    trace.logs = result.logs ?? [];
                    if (ctx.general.nationId === 0) {
                        return result;
                    }
                    turnTraces.push(trace);
                    return result;
                },
            }),
            turnProcessorOptions: {
                tickMinutes: 10,
                afterExecuteGeneral: async (general, result) => {
                    const trace = traceByGeneralId.get(general.id);
                    if (!trace) {
                        return;
                    }
                    trace.ok = result.ok;
                    trace.error = result.error;
                },
            },
        });
        const checkpointGoldByGeneral = new Map<number, number>();

        const dumpTraceSummary = (title: string, limit = 50) => {
            const recent = turnTraces.slice(-limit);
            console.log(`\n[TRACE] ${title} (last ${recent.length})`);
            for (const trace of recent) {
                const action = trace.actionText.replace(/\s+/g, ' ').trim();
                const extra = trace.usedFallback
                    ? ` fallback(${trace.requestedAction} -> ${trace.actionKey}) ${trace.blockedReason ?? ''}`
                    : ` ${trace.requestedAction} -> ${trace.actionKey}`;
                console.log(
                    `- ${trace.year}-${String(trace.month).padStart(2, '0')} N${trace.nationId} G${trace.generalId} g${round(trace.gold)}/r${round(trace.rice)} c${trace.crew}/t${trace.train}/a${trace.atmos} ` +
                        `${trace.ok ? 'OK' : 'FAIL'} ${action}${extra}`
                );
            }
        };

        const debug = createWorldDebugger(() => worldRef.current, {
            includeNationSummary: true,
        });
        const assertUprisingCount = (minCount: number) => {
            const nations = world.listNations();
            expect(nations.length).toBeGreaterThanOrEqual(minCount);
        };

        const assertFoundedCount = (minCount: number) => {
            const nations = world.listNations().filter((nation) => nation.level >= 1 && nation.capitalCityId);
            expect(nations.length).toBeGreaterThanOrEqual(minCount);
        };

        const assertTaxRateUnder = (limit: number) => {
            const founded = world.listNations().filter((nation) => nation.level >= 1 && nation.capitalCityId);
            for (const nation of founded) {
                const rate = typeof nation.meta.rate === 'number' ? nation.meta.rate : 20;
                expect(rate).toBeLessThan(limit);
            }
        };

        const assertSmallMediumCitiesFounded = () => {
            const targetCities = world.listCities().filter((city) => [4, 5].includes(city.level));
            for (const city of targetCities) {
                expect(city.nationId).toBeGreaterThan(0);
            }
        };

        const assertCityTrust = (minTrust: number) => {
            const targetCities = world.listCities().filter((city) => city.nationId > 0);
            for (const city of targetCities) {
                const trust = typeof city.meta.trust === 'number' ? city.meta.trust : 0;
                expect(trust).toBeGreaterThanOrEqual(minTrust);
            }
        };

        const assertNationGeneralCount = (targetCount: number) => {
            const nations = world.listNations().filter((nation) => nation.level >= 1 && nation.capitalCityId);
            const generals = world.listGenerals();
            for (const nation of nations) {
                const nationGenerals = generals.filter((general) => general.nationId === nation.id);
                expect(nationGenerals.length).toBeGreaterThanOrEqual(targetCount);
            }
        };

        const assertWarReadiness = (minReadyCount: number, minTrain: number, minAtmos: number) => {
            const recruited = world
                .listGenerals()
                .filter((general) => general.nationId > 0 && general.crew > 0 && general.crewTypeId > 0);
            const maxTrain = recruited.reduce((max, general) => Math.max(max, general.train), 0);
            const maxAtmos = recruited.reduce((max, general) => Math.max(max, general.atmos), 0);
            const ready = recruited.filter((general) => general.train >= minTrain && general.atmos >= minAtmos);

            expect(recruited.length).toBeGreaterThan(0);
            expect(maxTrain).toBeGreaterThan(0);
            expect(maxAtmos).toBeGreaterThan(0);
            expect(ready.length).toBeGreaterThanOrEqual(minReadyCount);
        };

        const assertDispatchRecorded = (year: number, month: number, minCount = 1) => {
            const matches = turnTraces.filter(
                (trace) => trace.year === year && trace.month === month && trace.actionKey === 'che_출병'
            );
            expect(matches.length).toBeGreaterThanOrEqual(minCount);
        };

        const assertNoNeutralCities = () => {
            const neutralCities = world.listCities().filter((city) => city.nationId <= 0);
            expect(neutralCities.length).toBe(0);
        };

        const maybeSnapshotGold = () => {
            checkpointGoldByGeneral.clear();
            for (const general of world.listGenerals()) {
                if (general.nationId > 0) {
                    checkpointGoldByGeneral.set(general.id, general.gold);
                }
            }
        };

        const assertGoldIncome = () => {
            const generals = world.listGenerals().filter((general) => general.nationId > 0);
            let beforeTotal = 0;
            let afterTotal = 0;
            for (const general of generals) {
                beforeTotal += checkpointGoldByGeneral.get(general.id) ?? 0;
                afterTotal += general.gold;
            }
            expect(afterTotal).toBeGreaterThan(beforeTotal);
        };

        const assertDomesticGrowth = () => {
            const citiesNow = world.listCities().filter((city) => city.nationId > 0);
            for (const city of citiesNow) {
                const baseline = initialCityStats.get(city.id);
                if (!baseline) {
                    continue;
                }
                // 182년 시점에는 징병이 병행되어 인구가 순감할 수 있으므로, 대규모 붕괴만 방지한다.
                expect(city.population).toBeGreaterThan(baseline.population - 15000);
                expect(city.agriculture).toBeGreaterThan(baseline.agriculture);
                expect(city.commerce).toBeGreaterThan(baseline.commerce);
                expect(city.security).toBeGreaterThan(baseline.security);
                expect(city.defence).toBeGreaterThan(baseline.defence);
                expect(city.wall).toBeGreaterThan(baseline.wall);
            }
        };

        const targetChecks = new Map<string, () => void>([
            ['179-09', () => assertUprisingCount(1)],
            ['179-10', () => assertUprisingCount(2)],
            ['179-11', () => assertFoundedCount(1)],
            [
                '179-12',
                () => {
                    assertTaxRateUnder(15);
                    maybeSnapshotGold();
                },
            ],
            ['180-01', () => assertGoldIncome()],
            ['180-07', () => assertSmallMediumCitiesFounded()],
            ['180-11', () => assertCityTrust(90)],
            ['181-01', () => assertNationGeneralCount(10)],
            ['182-01', () => assertDomesticGrowth()],
            ['183-01', () => assertWarReadiness(10, 70, 70)],
            ['183-02', () => assertDispatchRecorded(183, 1, 1)],
            ['183-07', () => assertNoNeutralCities()],
        ]);

        const toKey = (year: number, month: number) => `${year}-${String(month).padStart(2, '0')}`;

        const debugRecruitConstraints = async (generalId: number) => {
            const general = world.getGeneralById(generalId);
            if (!general) {
                console.log('[DEBUG] no general for recruit check', generalId);
                return;
            }
            const city = world.getCityById(general.cityId);
            const nation = general.nationId > 0 ? world.getNationById(general.nationId) : null;
            const { general: generalDefinitions, nation: nationDefinitions } = await buildReservedTurnDefinitions({
                env: commandEnv,
                commandProfile: DEFAULT_TURN_COMMAND_PROFILE,
                defaultActionKey: '휴식',
            });
            const definition = generalDefinitions.get('che_징병');
            if (!definition) {
                console.log('[DEBUG] no che_징병 definition');
                return;
            }
            const args = {
                crewType: snapshot.unitSet?.defaultCrewTypeId ?? 1100,
                amount: general.stats.leadership * 100,
            };
            const parsedArgs = definition.parseArgs(args);
            if (!parsedArgs) {
                console.log('[DEBUG] che_징병 args invalid', args);
                return;
            }

            const state = world.getState();
            const startYear = snapshot.scenarioMeta?.startYear ?? 0;
            const constraintEnv: Record<string, unknown> = {
                currentYear: state.currentYear,
                currentMonth: state.currentMonth,
                year: state.currentYear,
                month: state.currentMonth,
                startYear,
                relYear: state.currentYear - startYear,
                openingPartYear: commandEnv.openingPartYear,
                minAvailableRecruitPop: commandEnv.minAvailableRecruitPop,
                cities: world.listCities(),
                nations: world.listNations(),
                map: LARGE_TEST_MAP,
                unitSet: snapshot.unitSet,
            };

            const ctx: ConstraintContext = {
                actorId: general.id,
                cityId: general.cityId,
                nationId: general.nationId,
                args: parsedArgs as Record<string, unknown>,
                env: constraintEnv,
                mode: 'full',
            };

            const view: StateView = {
                has: (req: RequirementKey) => {
                    if (req.kind === 'general') return world.getGeneralById(req.id) !== null;
                    if (req.kind === 'city') return world.getCityById(req.id) !== null;
                    if (req.kind === 'nation') return world.getNationById(req.id) !== null;
                    if (req.kind === 'generalList') return true;
                    if (req.kind === 'nationList') return true;
                    if (req.kind === 'env') return true;
                    return false;
                },
                get: (req: RequirementKey) => {
                    if (req.kind === 'general') return world.getGeneralById(req.id);
                    if (req.kind === 'city') return world.getCityById(req.id);
                    if (req.kind === 'nation') return world.getNationById(req.id);
                    if (req.kind === 'generalList') return world.listGenerals();
                    if (req.kind === 'nationList') return world.listNations();
                    if (req.kind === 'env') return constraintEnv[req.key] ?? null;
                    return null;
                },
            };

            const constraints = definition.buildConstraints(ctx, parsedArgs as never);
            const result = evaluateConstraints(constraints, ctx, view);
            console.log('[DEBUG] che_징병 constraint result:', result);
            for (const constraint of constraints) {
                const single = evaluateConstraints([constraint], ctx, view);
                if (single.kind !== 'allow') {
                    console.log('[DEBUG] constraint blocked:', constraint.name, single);
                }
            }

            const generalFallback = generalDefinitions.get('휴식')!;
            const nationFallback = nationDefinitions.get('휴식')!;
            const ai = new GeneralAI({
                general,
                city: city ?? undefined,
                nation,
                world: state,
                worldRef: world,
                reservedTurnProvider: reservedTurnStore,
                scenarioConfig: snapshot.scenarioConfig,
                scenarioMeta: snapshot.scenarioMeta,
                map: LARGE_TEST_MAP,
                unitSet: snapshot.unitSet,
                commandEnv,
                generalDefinitions,
                nationDefinitions,
                generalFallback,
                nationFallback,
            });
            console.log('[DEBUG] can 징병:', ai.generalPolicy.can('징병'));
            console.log('[DEBUG] priority includes 징병:', ai.generalPolicy.priority.includes('징병'));
            console.log('[DEBUG] do징병 candidate:', do징병(ai));
        };

        try {
            await runUntil(
                (current) => current.currentYear > 183 || (current.currentYear === 183 && current.currentMonth >= 7),
                undefined,
                (current) => {
                    const key = toKey(current.currentYear, current.currentMonth);
                    const checker = targetChecks.get(key);
                    if (checker) {
                        checker();
                    }
                }
            );
        } catch (error) {
            const lastAiTrace = [...turnTraces].reverse().find((trace) => trace.aiState);
            if (lastAiTrace?.aiState) {
                console.log('[DEBUG] last aiState:', lastAiTrace.aiState);
            }
            const debugGeneralId = lastAiTrace?.generalId ?? world.listGenerals()[0]?.id;
            if (debugGeneralId) {
                await debugRecruitConstraints(debugGeneralId);
            }
            dumpTraceSummary('NPC 대형 시뮬레이션 실패', 200);
            debug.dumpWorldSummary('NPC 대형 시뮬레이션 실패');
            const sampleNation = world.listNations().find((nation) => nation.level >= 1 && nation.capitalCityId);
            if (sampleNation) {
                const policy = (sampleNation.meta as Record<string, unknown>)?.npc_nation_policy;
                console.log('[TRACE] sample npc_nation_policy:', policy);
                debug.dumpNation(sampleNation.id, 'NPC 샘플 국가');
                const sampleGeneral = world
                    .listGenerals()
                    .find((general) => general.nationId === sampleNation.id && general.cityId > 0);
                if (sampleGeneral) {
                    const city = world.getCityById(sampleGeneral.cityId);
                    console.log('[TRACE] sample recruit check:', {
                        generalId: sampleGeneral.id,
                        leadership: sampleGeneral.stats.leadership,
                        cityId: sampleGeneral.cityId,
                        population: city?.population,
                        populationMax: city?.populationMax,
                    });
                    debug.dumpCity(sampleGeneral.cityId, 'NPC 샘플 도시');
                }
                const nationGenerals = world.listGenerals().filter((general) => general.nationId === sampleNation.id);
                const crewOnly = nationGenerals.filter((general) => general.crew > 0);
                const crewWithType = crewOnly.filter((general) => general.crewTypeId > 0);
                console.log('[TRACE] crew stats:', {
                    totalGenerals: nationGenerals.length,
                    crewOnly: crewOnly.length,
                    crewWithType: crewWithType.length,
                });
            }
            throw error;
        }
    }, 30_000);
});
