import { createGamePostgresConnector, type InputJsonValue, type TurnEngineEventCreateManyInput } from '@sammo-ts/infra';
import { asRecord } from '@sammo-ts/common';
import { buildScenarioBootstrap, type GeneralMeta, type ScenarioBootstrapWarning, type WorldSeedPayload } from '@sammo-ts/logic';

import type { MapLoaderOptions } from './mapLoader.js';
import { loadMapDefinitionByName } from './mapLoader.js';
import type { ScenarioLoaderOptions } from './scenarioLoader.js';
import { loadScenarioDefinitionById } from './scenarioLoader.js';
import type { UnitSetLoaderOptions } from './unitSetLoader.js';
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
import { applyInitialChangeCityEvents } from '../turn/monthlyChangeCityAction.js';

const DEFAULT_TICK_SECONDS = 120 * 60;
const DEFAULT_GENERAL_GOLD = 1000;
const DEFAULT_GENERAL_RICE = 1000;
const DEFAULT_OPENING_PART_YEAR = 3;
const INTEGRATION_WORLD_SEED_ENV = 'INTEGRATION_WORLD_SEED';

const MINUTES_TO_MS = 60_000;

export interface ScenarioAutorunOptions {
    limitMinutes: number;
    options: Record<string, boolean>;
}

export interface ScenarioInstallOptions {
    turnTermMinutes?: number;
    sync?: boolean;
    fiction?: number;
    extend?: boolean;
    blockGeneralCreate?: number;
    npcMode?: number;
    showImgLevel?: number;
    tournamentTrig?: boolean;
    joinMode?: 'full' | 'onlyRandom';
    autorunUser?: ScenarioAutorunOptions | null;
    preopenAt?: Date | null;
    season?: number;
    serverId?: string;
}

export interface ScenarioSeedOptions {
    scenarioId: number;
    databaseUrl: string;
    scenarioOptions?: ScenarioLoaderOptions;
    mapOptions?: MapLoaderOptions;
    unitSetOptions?: UnitSetLoaderOptions;
    resetTables?: boolean;
    now?: Date;
    tickSeconds?: number;
    installOptions?: ScenarioInstallOptions;
    includeNeutralNationInSeed?: boolean;
    defaultGeneralGold?: number;
    defaultGeneralRice?: number;
}

export interface ScenarioSeedResult {
    seed: WorldSeedPayload;
    warnings: ScenarioBootstrapWarning[];
}

const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;

type RawQueryClient = {
    $queryRawUnsafe<T>(query: string): Promise<T>;
};

const resolveSchemaName = (databaseUrl: string): string => {
    try {
        return new URL(databaseUrl).searchParams.get('schema') ?? 'public';
    } catch {
        return 'public';
    }
};

const hasEventTable = async (prisma: RawQueryClient, schema: string): Promise<boolean> => {
    try {
        const result = await prisma.$queryRawUnsafe<Array<{ regclass: string | null }>>(
            `SELECT to_regclass('${schema}.event')::text as regclass`
        );
        return Array.isArray(result) && result.length > 0 && result[0]?.regclass !== null;
    } catch {
        return false;
    }
};

const formatDateTime = (date: Date): string => {
    const pad = (value: number): string => String(value).padStart(2, '0');
    return [
        `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
        `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`,
    ].join(' ');
};

const cutTurn = (date: Date, turnTermMinutes: number): Date => {
    const base = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0, 0);
    base.setDate(base.getDate() - 1);
    const diffMinutes = Math.floor((date.getTime() - base.getTime()) / MINUTES_TO_MS);
    const alignedMinutes = diffMinutes - (diffMinutes % turnTermMinutes);
    return new Date(base.getTime() + alignedMinutes * MINUTES_TO_MS);
};

const cutDay = (date: Date, turnTermMinutes: number): { startTime: Date; month: number; yearPulled: boolean } => {
    const base = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0, 0);
    base.setDate(base.getDate() - 1);
    const baseGap = 12 * turnTermMinutes;
    const diffMinutes = Math.floor((date.getTime() - base.getTime()) / MINUTES_TO_MS);
    const timeAdjust = diffMinutes % baseGap;
    const month = Math.floor(timeAdjust / turnTermMinutes) + 1;
    const yearPulled = month > 3;
    const alignedMinutes = diffMinutes - timeAdjust + (yearPulled ? baseGap : 0);
    return {
        startTime: new Date(base.getTime() + alignedMinutes * MINUTES_TO_MS),
        month,
        yearPulled,
    };
};

const resolveStartState = (
    scenarioStartYear: number | null,
    now: Date,
    turnTermMinutes: number,
    sync: boolean
): { startTime: Date; currentYear: number; currentMonth: number } => {
    const startYear = scenarioStartYear ?? 0;
    if (!sync) {
        return {
            startTime: cutTurn(now, turnTermMinutes),
            currentYear: startYear,
            currentMonth: 1,
        };
    }

    const { startTime, month, yearPulled } = cutDay(now, turnTermMinutes);
    return {
        startTime,
        currentYear: startYear - (yearPulled ? 1 : 0),
        currentMonth: month,
    };
};

const resolveGeneralAge = (startYear: number | null, birthYear: number): number => {
    if (startYear === null || birthYear <= 0) {
        return 20;
    }
    return Math.max(startYear - birthYear, 0);
};

const resolveKillturnFromDeathYear = (
    currentYear: number,
    currentMonth: number,
    deathYear: number,
    fallback: number
): number => {
    if (!Number.isFinite(deathYear) || deathYear <= 0) {
        return fallback;
    }
    const deathMonth = Math.floor(Math.random() * 12) + 1;
    const diff = (deathYear - currentYear) * 12 + (deathMonth - currentMonth);
    return Math.max(diff, 0);
};

const buildEventRows = (rows: unknown[], targetOverride?: string): TurnEngineEventCreateManyInput[] => {
    const result: TurnEngineEventCreateManyInput[] = [];

    for (const row of rows) {
        if (!Array.isArray(row)) {
            continue;
        }
        if (targetOverride) {
            const [condition, ...actions] = row;
            result.push({
                targetCode: targetOverride,
                priority: 0,
                condition: asJson(condition ?? null),
                action: asJson(actions),
                meta: asJson({ source: targetOverride }),
            });
            continue;
        }

        const [target, priority, condition, ...actions] = row;
        if (typeof target !== 'string' || typeof priority !== 'number') {
            continue;
        }
        result.push({
            targetCode: target,
            priority,
            condition: asJson(condition ?? null),
            action: asJson(actions),
            meta: asJson({ source: 'scenario' }),
        });
    }

    return result;
};

// 시나리오 초기 데이터를 로드해 DB에 저장한다.
export const seedScenarioToDatabase = async (options: ScenarioSeedOptions): Promise<ScenarioSeedResult> => {
    const install = options.installOptions;
    const scenario = await loadScenarioDefinitionById(options.scenarioId, options.scenarioOptions);
    const includeExtendedGeneral = install?.extend ?? true;
    const scenarioDefinition = includeExtendedGeneral ? scenario : { ...scenario, generalsEx: [] };
    const map = await loadMapDefinitionByName(scenario.config.environment.mapName, options.mapOptions);
    const unitSet = await loadUnitSetDefinitionByName(scenario.config.environment.unitSet, options.unitSetOptions);

    const { seed, warnings } = buildScenarioBootstrap({
        scenario: scenarioDefinition,
        map,
        unitSet,
        options: {
            includeNeutralNationInSeed: options.includeNeutralNationInSeed ?? true,
        },
    });
    seed.cities = applyInitialChangeCityEvents(seed.cities, seed.initialEvents);

    const connector = createGamePostgresConnector({ url: options.databaseUrl });
    const now = options.now ?? new Date();
    const tickSeconds =
        install?.turnTermMinutes !== undefined ? install.turnTermMinutes * 60 : options.tickSeconds ?? DEFAULT_TICK_SECONDS;
    const turnTermMinutes = Math.max(1, Math.round(tickSeconds / 60));
    const sync = install?.sync ?? false;
    const startState = resolveStartState(scenario.startYear ?? null, now, turnTermMinutes, sync);
    const generalGold = options.defaultGeneralGold ?? DEFAULT_GENERAL_GOLD;
    const generalRice = options.defaultGeneralRice ?? DEFAULT_GENERAL_RICE;

    const scenarioConst = asRecord(seed.scenarioConfig.const);
    if (
        typeof scenarioConst.openingPartYear !== 'number' ||
        Number.isNaN(scenarioConst.openingPartYear)
    ) {
        scenarioConst.openingPartYear = DEFAULT_OPENING_PART_YEAR;
    }
    const scenarioConfig = {
        ...seed.scenarioConfig,
        const: scenarioConst,
    };

    const worldConfig: Record<string, unknown> = {
        fiction: install?.fiction,
        fictionMode: install?.fiction === 0 ? '연의' : install?.fiction === 1 ? '가상' : undefined,
        joinMode: install?.joinMode,
        blockGeneralCreate: install?.blockGeneralCreate,
        npcMode: install?.npcMode,
        showImgLevel: install?.showImgLevel,
        tournamentTrig: install?.tournamentTrig,
        extendedGeneral: includeExtendedGeneral,
        turnTermMinutes: install?.turnTermMinutes,
        syncTurnTime: install?.sync,
    };

    const worldMeta: Record<string, unknown> = {
        scenarioId: options.scenarioId,
        scenarioMeta: seed.scenarioMeta,
        starttime: formatDateTime(startState.startTime),
        turntime: formatDateTime(now),
        opentime: formatDateTime(now),
        lastTurnTime: formatDateTime(now),
    };

    if (typeof install?.season === 'number' && Number.isFinite(install.season)) {
        worldMeta.season = Math.floor(install.season);
    }
    if (typeof install?.serverId === 'string' && install.serverId.trim()) {
        worldMeta.serverId = install.serverId.trim();
    }

    const integrationSeed = process.env[INTEGRATION_WORLD_SEED_ENV];
    if (typeof integrationSeed === 'string' && integrationSeed.trim().length > 0) {
        worldMeta.hiddenSeed = integrationSeed.trim();
    }

    if (install?.preopenAt) {
        worldMeta.preopenAt = formatDateTime(install.preopenAt);
    }

    if (install?.autorunUser) {
        worldMeta.autorun_user = {
            limit_minutes: install.autorunUser.limitMinutes,
            options: install.autorunUser.options,
        };
    }

    await connector.connect();
    try {
        const prisma = connector.prisma;
        const schema = resolveSchemaName(options.databaseUrl);
        const eventTableReady = await hasEventTable(prisma, schema);

        if (options.resetTables ?? true) {
            if (eventTableReady) {
                await prisma.event.deleteMany();
            }
            await prisma.diplomacy.deleteMany();
            await prisma.general.deleteMany();
            await prisma.troop.deleteMany();
            await prisma.city.deleteMany();
            await prisma.nation.deleteMany();
            await prisma.worldState.deleteMany();
        }

        await prisma.worldState.create({
            data: {
                scenarioCode: String(options.scenarioId),
                currentYear: startState.currentYear,
                currentMonth: startState.currentMonth,
                tickSeconds,
                config: asJson({ ...scenarioConfig, ...worldConfig }),
                meta: asJson(worldMeta),
            },
        });

        if (typeof worldMeta.serverId === 'string' && worldMeta.serverId) {
            await prisma.gameHistory.upsert({
                where: { serverId: worldMeta.serverId },
                create: {
                    serverId: worldMeta.serverId,
                    date: now,
                    winnerNation: null,
                    map: scenario.config.environment.mapName ?? null,
                    season:
                        typeof worldMeta.season === 'number' && Number.isFinite(worldMeta.season)
                            ? Math.floor(worldMeta.season)
                            : 1,
                    scenario: options.scenarioId,
                    scenarioName: String(seed.scenarioMeta?.title ?? ''),
                    env: asJson({
                        config: scenarioConfig,
                        meta: worldMeta,
                    }),
                },
                update: {
                    date: now,
                    winnerNation: null,
                    map: scenario.config.environment.mapName ?? null,
                    season:
                        typeof worldMeta.season === 'number' && Number.isFinite(worldMeta.season)
                            ? Math.floor(worldMeta.season)
                            : 1,
                    scenario: options.scenarioId,
                    scenarioName: String(seed.scenarioMeta?.title ?? ''),
                    env: asJson({
                        config: scenarioConfig,
                        meta: worldMeta,
                    }),
                },
            });
        }

        if (seed.nations.length > 0) {
            await prisma.nation.createMany({
                data: seed.nations.map((nation) => ({
                    id: nation.id,
                    name: nation.name,
                    color: nation.color,
                    capitalCityId: nation.capitalCityId ?? null,
                    gold: nation.gold,
                    rice: nation.rice,
                    tech: nation.tech,
                    level: nation.level,
                    typeCode: nation.typeCode,
                    meta: asJson({
                        infoText: nation.infoText,
                        cityIds: nation.cityIds,
                    }),
                })),
            });
        }

        if (seed.cities.length > 0) {
            await prisma.city.createMany({
                data: seed.cities.map((city) => ({
                    id: city.id,
                    name: city.name,
                    level: city.level,
                    nationId: city.nationId,
                    supplyState: city.supplyState,
                    frontState: city.frontState,
                    population: city.population,
                    populationMax: city.populationMax,
                    agriculture: city.agriculture,
                    agricultureMax: city.agricultureMax,
                    commerce: city.commerce,
                    commerceMax: city.commerceMax,
                    security: city.security,
                    securityMax: city.securityMax,
                    trust: city.trust,
                    trade: city.trade,
                    defence: city.defence,
                    defenceMax: city.defenceMax,
                    wall: city.wall,
                    wallMax: city.wallMax,
                    region: city.region,
                    conflict: asJson({}),
                    meta: asJson({
                        position: city.position,
                        connections: city.connections,
                        state: city.state,
                        ...city.meta,
                    }),
                })),
            });
        }

        if (seed.generals.length > 0) {
            await prisma.general.createMany({
                data: seed.generals.map((general) => ({
                    id: general.id,
                    name: general.name,
                    nationId: general.nationId,
                    cityId: general.cityId,
                    npcState: general.npcType,
                    affinity: general.affinity,
                    bornYear: general.birthYear,
                    deadYear: general.deathYear,
                    picture: general.picture === null ? null : String(general.picture),
                    leadership: general.stats.leadership,
                    strength: general.stats.strength,
                    intel: general.stats.intelligence,
                    officerLevel: general.officerLevel,
                    gold: generalGold,
                    rice: generalRice,
                    crewTypeId: general.crewTypeId,
                    horseCode: general.horse ?? 'None',
                    weaponCode: general.weapon ?? 'None',
                    bookCode: general.book ?? 'None',
                    itemCode: general.item ?? 'None',
                    turnTime: now,
                    age: resolveGeneralAge(scenario.startYear ?? null, general.birthYear),
                    startAge: resolveGeneralAge(scenario.startYear ?? null, general.birthYear),
                    personalCode: general.personality ?? 'None',
                    specialCode: general.special ?? 'None',
                    special2Code: general.specialWar ?? 'None',
                    lastTurn: asJson({}),
                    meta: asJson(
                        (() => {
                            const meta = { ...general.meta } as Record<string, unknown>;
                            if (typeof meta.birthYear !== 'number' || !Number.isFinite(meta.birthYear)) {
                                meta.birthYear = general.birthYear;
                            }
                            delete meta.deathYear;
                            delete meta.deadYear;
                            const fallbackKillturn =
                                typeof meta.killturn === 'number' && Number.isFinite(meta.killturn) ? meta.killturn : 0;
                            const killturn = resolveKillturnFromDeathYear(
                                startState.currentYear,
                                startState.currentMonth,
                                general.deathYear,
                                fallbackKillturn
                            );
                            return {
                                ...meta,
                                killturn,
                                npcType: general.npcType,
                                crewTypeId: general.crewTypeId,
                            } satisfies GeneralMeta;
                        })()
                    ),
                    penalty: asJson({}),
                })),
            });
        }

        if (seed.troops.length > 0) {
            await prisma.troop.createMany({
                data: seed.troops.map((troop) => ({
                    troopLeaderId: troop.id,
                    nationId: troop.nationId,
                    name: troop.name,
                })),
            });
        }

        const diplomacyMap = new Map<
            string,
            { srcNationId: number; destNationId: number; state: number; term: number }
        >();
        const nationIds = seed.nations.map((nation) => nation.id);
        for (const srcNationId of nationIds) {
            for (const destNationId of nationIds) {
                if (srcNationId === destNationId) {
                    continue;
                }
                diplomacyMap.set(`${srcNationId}:${destNationId}`, {
                    srcNationId,
                    destNationId,
                    state: 2,
                    term: 0,
                });
            }
        }
        for (const row of seed.diplomacy) {
            diplomacyMap.set(`${row.fromNationId}:${row.toNationId}`, {
                srcNationId: row.fromNationId,
                destNationId: row.toNationId,
                state: row.state,
                term: row.durationMonths,
            });
            diplomacyMap.set(`${row.toNationId}:${row.fromNationId}`, {
                srcNationId: row.toNationId,
                destNationId: row.fromNationId,
                state: row.state,
                term: row.durationMonths,
            });
        }
        const diplomacyRows = Array.from(diplomacyMap.values());
        if (diplomacyRows.length > 0) {
            await prisma.diplomacy.createMany({
                data: diplomacyRows.map((row) => ({
                    srcNationId: row.srcNationId,
                    destNationId: row.destNationId,
                    stateCode: row.state,
                    term: row.term,
                    meta: asJson({}),
                })),
            });
        }

        const eventRows = buildEventRows(seed.events);
        if (eventRows.length > 0 && eventTableReady) {
            await prisma.event.createMany({
                data: eventRows,
            });
        }
    } finally {
        await connector.disconnect();
    }

    return { seed, warnings };
};
