import {
    createGamePostgresConnector,
    type JsonValue,
    type TurnEngineCityRow,
    type TurnEngineDatabaseClient,
    type TurnEngineDiplomacyRow,
    type TurnEngineGeneralRow,
    type TurnEngineGeneralAccessLogRow,
    type TurnEngineInheritancePointRow,
    type TurnEngineRankDataRow,
    type TurnEngineNationRow,
    type TurnEngineTroopRow,
} from '@sammo-ts/infra';
import type {
    City,
    GeneralItemSlots,
    GeneralLastTurn,
    Nation,
    ScenarioConfig,
    ScenarioMeta,
    Troop,
    TriggerValue,
} from '@sammo-ts/logic';
import { projectItemSlots, readItemInventoryFromMeta } from '@sammo-ts/logic/items/index.js';
import { z } from 'zod';
import { asRecord, isRecord } from '@sammo-ts/common';

import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
import type { MapLoaderOptions } from '../scenario/mapLoader.js';
import { loadMapDefinitionByName } from '../scenario/mapLoader.js';
import type { UnitSetLoaderOptions } from '../scenario/unitSetLoader.js';
import { loadUnitSetDefinitionByName } from '../scenario/unitSetLoader.js';
import type { TurnDiplomacy, TurnEvent, TurnGeneral, TurnWorldLoadResult } from './types.js';
import { readDiplomacyMeta } from '@sammo-ts/logic';
import { applyPersistedRankRowsToMeta } from './rankData.js';

interface TurnWorldLoaderOptions {
    databaseUrl: string;
    mapOptions?: MapLoaderOptions;
    unitSetOptions?: UnitSetLoaderOptions;
}

type JsonRecord = Record<string, unknown>;

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

const normalizeGeneralLastTurn = (value: unknown): GeneralLastTurn => {
    const raw = asRecord(value);
    const arg = asRecord(raw.arg);
    return {
        command: typeof raw.command === 'string' ? raw.command : '휴식',
        ...(Object.keys(arg).length > 0 ? { arg } : {}),
        ...(typeof raw.term === 'number' && Number.isFinite(raw.term) ? { term: Math.floor(raw.term) } : {}),
        ...(typeof raw.seq === 'number' && Number.isFinite(raw.seq) ? { seq: Math.floor(raw.seq) } : {}),
    };
};

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

const readMetaNumber = (meta: Record<string, unknown>, 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 zScenarioStatBlock = z.object({
    total: z.number(),
    min: z.number(),
    max: z.number(),
    npcTotal: z.number(),
    npcMax: z.number(),
    npcMin: z.number(),
    chiefMin: z.number(),
});

const zScenarioEnvironment = z.object({
    mapName: z.string(),
    unitSet: z.string(),
    scenarioEffect: z.union([z.string(), z.null()]).optional(),
});

const zScenarioConfig = z.object({
    stat: zScenarioStatBlock,
    iconPath: z.string(),
    map: z.record(z.string(), z.unknown()),
    const: z.record(z.string(), z.unknown()),
    environment: zScenarioEnvironment,
});

const zScenarioMeta = z.object({
    title: z.string(),
    startYear: z.number().nullable(),
    life: z.number().nullable(),
    fiction: z.number().nullable(),
    history: z.array(z.string()),
    ignoreDefaultEvents: z.boolean(),
});

const parseScenarioMeta = (meta: JsonRecord): ScenarioMeta | undefined => {
    const raw = meta.scenarioMeta;
    const parsed = zScenarioMeta.safeParse(raw);
    return parsed.success ? parsed.data : undefined;
};

const parseLastTurnTime = (meta: JsonRecord): Date | null => {
    const raw = meta.lastTurnTime;
    if (typeof raw !== 'string') {
        return null;
    }
    const parsed = new Date(raw);
    if (Number.isNaN(parsed.getTime())) {
        return null;
    }
    return parsed;
};

const resolveFallbackTurnTimeBase = (generals: TurnGeneral[], updatedAt: Date | null): Date => {
    let earliest: Date | null = null;
    for (const general of generals) {
        const turnTime = general.turnTime;
        if (!earliest || turnTime.getTime() < earliest.getTime()) {
            earliest = turnTime;
        }
    }
    if (earliest) {
        return earliest;
    }
    if (updatedAt) {
        return updatedAt;
    }
    return new Date();
};

const alignToPreviousTick = (base: Date, tickMinutes: number): Date => {
    const nextTick = getNextTickTime(base, tickMinutes);
    return new Date(nextTick.getTime() - tickMinutes * 60_000);
};

const mapScenarioConfig = (raw: JsonValue): ScenarioConfig => {
    const parsed = zScenarioConfig.safeParse(raw);
    if (!parsed.success) {
        throw new Error(`world_state.config is invalid: ${parsed.error.message}`);
    }
    return parsed.data;
};

const mapGeneralRow = (
    row: TurnEngineGeneralRow,
    rankRows: readonly TurnEngineRankDataRow[],
    inheritanceRows: readonly TurnEngineInheritancePointRow[],
    accessRow?: TurnEngineGeneralAccessLogRow
): TurnGeneral => {
    const legacySlots: GeneralItemSlots = {
        horse: normalizeCode(row.horseCode),
        weapon: normalizeCode(row.weaponCode),
        book: normalizeCode(row.bookCode),
        item: normalizeCode(row.itemCode),
    };
    const rawMeta = { ...(asTriggerRecord(row.meta) as Record<string, unknown>) };
    applyPersistedRankRowsToMeta(rawMeta, rankRows);
    const inheritancePoints = Object.fromEntries(inheritanceRows.map((entry) => [entry.key, entry.value]));
    const itemInventory = readItemInventoryFromMeta(rawMeta, legacySlots);
    return {
        ...((): { meta: TurnGeneral['meta'] } => {
            const meta = rawMeta;
            const killturn = readMetaNumber(meta, 'killturn');
            if (killturn === null) {
                throw new Error(`general.meta.killturn is required (generalId=${row.id}).`);
            }
            return { meta: { ...meta, killturn } as TurnGeneral['meta'] };
        })(),
        id: row.id,
        userId: row.userId,
        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,
        startAge: row.startAge,
        npcState: row.npcState,
        bornYear: row.bornYear,
        deadYear: row.deadYear,
        affinity: row.affinity,
        picture: row.picture,
        triggerState: {
            flags: {},
            counters: {},
            modifiers: {},
            meta: {},
        },
        itemInventory,
        lastTurn: normalizeGeneralLastTurn(row.lastTurn),
        penalty: row.penalty,
        // meta는 상단에서 보장 처리됨.
        turnTime: row.turnTime,
        recentWarTime: row.recentWarTime ?? null,
        inheritancePoints,
        ...(accessRow ? { refreshScoreTotal: accessRow.refreshScoreTotal } : {}),
    };
};

const mapCityRow = (row: TurnEngineCityRow): 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,
        conflict: asTriggerRecord(row.conflict),
        meta: {
            ...meta,
            trust: row.trust,
            ...(row.trade === null ? {} : { trade: row.trade }),
            region: row.region,
        },
    };
};

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

const mapDiplomacyRow = (row: TurnEngineDiplomacyRow): TurnDiplomacy => {
    const { meta, dead } = readDiplomacyMeta(asRecord(row.meta));
    return {
        fromNationId: row.srcNationId,
        toNationId: row.destNationId,
        state: row.stateCode,
        term: row.term,
        dead,
        meta,
    };
};

const mapEventRow = (row: {
    id: number;
    targetCode: string;
    priority: number;
    condition: JsonValue;
    action: JsonValue;
    meta: JsonValue;
}): TurnEvent => ({
    id: row.id,
    targetCode: row.targetCode,
    priority: row.priority,
    condition: row.condition,
    action: row.action,
    meta: asRecord(row.meta),
});

const mapTroopRow = (row: TurnEngineTroopRow): Troop => ({
    id: row.troopLeaderId,
    nationId: row.nationId,
    name: row.name,
});

export const loadTurnWorldFromDatabase = async (options: TurnWorldLoaderOptions): Promise<TurnWorldLoadResult> => {
    const connector = createGamePostgresConnector({ url: options.databaseUrl });
    await connector.connect();
    try {
        const prisma: TurnEngineDatabaseClient = connector.prisma;
        const worldState = await prisma.worldState.findFirst();
        if (!worldState) {
            throw new Error('world_state row is required to start turn daemon.');
        }

        const [
            generalRows,
            rankRows,
            inheritanceRows,
            accessRows,
            cityRows,
            nationRows,
            diplomacyRows,
            troopRows,
            eventRows,
        ] = await Promise.all([
            prisma.general.findMany(),
            prisma.rankData.findMany(),
            prisma.inheritancePoint.findMany(),
            prisma.generalAccessLog.findMany(),
            prisma.city.findMany(),
            prisma.nation.findMany(),
            prisma.diplomacy.findMany(),
            prisma.troop.findMany(),
            prisma.event.findMany({
                orderBy: [{ priority: 'desc' }, { id: 'asc' }],
            }),
        ]);

        const ranksByGeneral = new Map<number, TurnEngineRankDataRow[]>();
        for (const row of rankRows) {
            const bucket = ranksByGeneral.get(row.generalId) ?? [];
            bucket.push(row);
            ranksByGeneral.set(row.generalId, bucket);
        }
        const inheritanceByUser = new Map<string, TurnEngineInheritancePointRow[]>();
        for (const row of inheritanceRows) {
            const bucket = inheritanceByUser.get(row.userId) ?? [];
            bucket.push(row);
            inheritanceByUser.set(row.userId, bucket);
        }
        const accessByGeneral = new Map(accessRows.map((row) => [row.generalId, row]));
        const generals = generalRows.map((row) =>
            mapGeneralRow(
                row,
                ranksByGeneral.get(row.id) ?? [],
                row.userId ? (inheritanceByUser.get(row.userId) ?? []) : [],
                accessByGeneral.get(row.id)
            )
        );
        const cities = cityRows.map(mapCityRow);
        const nations = nationRows.map(mapNationRow);
        const diplomacy = diplomacyRows.map(mapDiplomacyRow);
        const troops = troopRows.map(mapTroopRow);

        const worldConfig = asRecord(worldState.config);
        const scenarioConfig = mapScenarioConfig(worldState.config);
        const mapName = scenarioConfig.environment?.mapName ?? 'che';
        const map = await loadMapDefinitionByName(mapName, options.mapOptions);
        const unitSetName = scenarioConfig.environment?.unitSet ?? 'che';
        const unitSet = await loadUnitSetDefinitionByName(unitSetName, options.unitSetOptions);

        const meta = asRecord(worldState.meta);
        const scenarioMeta = parseScenarioMeta(meta);

        const tickMinutes = Math.max(1, Math.round(worldState.tickSeconds / 60));
        const fallbackBase = resolveFallbackTurnTimeBase(generals, worldState.updatedAt ?? null);
        const lastTurnTime = parseLastTurnTime(meta) ?? alignToPreviousTick(fallbackBase, tickMinutes);

        const events = eventRows.filter((row) => row.targetCode !== 'initial').map(mapEventRow);
        const initialEvents = eventRows.filter((row) => row.targetCode === 'initial').map(mapEventRow);

        return {
            state: {
                id: worldState.id,
                currentYear: worldState.currentYear,
                currentMonth: worldState.currentMonth,
                tickSeconds: worldState.tickSeconds,
                lastTurnTime,
                meta,
            },
            snapshot: {
                scenarioConfig,
                ...(scenarioMeta ? { scenarioMeta } : {}),
                worldConfig,
                map,
                unitSet,
                nations,
                cities,
                generals,
                troops,
                diplomacy,
                events,
                initialEvents,
            },
        };
    } finally {
        await connector.disconnect();
    }
};
