import type { RandomGenerator } from '@sammo-ts/common';
import { asRecord, JosaUtil } from '@sammo-ts/common';
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { beNeutral, allowJoinAction } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
    GeneralActionEffect,
    GeneralActionOutcome,
    GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import { resolveStartYear } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';

export interface RandomAppointmentArgs {}

interface CandidateNation<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
    nation: Nation;
    generals: General<TriggerState>[];
    generalCount: number;
    monarchCityId: number | null;
    monarchAffinity: number;
}

export interface RandomAppointmentResolveContext<
    TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
    candidateNations: Array<CandidateNation<TriggerState>>;
    relYear: number;
    initialNationGenLimit: number;
    historicalNpcAffinityMode: boolean;
}

const ACTION_NAME = '무작위 국가로 임관';
const LEGACY_INITIAL_NATION_GEN_LIMIT = 10;
const LEGACY_DEFAULT_MAX_GENERAL = 500;

const TALK_LIST = [
    '어쩌다 보니',
    '인연이 닿아',
    '발길이 닿는 대로',
    '소문을 듣고',
    '점괘에 따라',
    '천거를 받아',
    '유명한',
    '뜻을 펼칠 곳을 찾아',
    '고향에 가까운',
    '천하의 균형을 맞추기 위해',
    '오랜 은거를 마치고',
];

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 readMetaBool = (meta: Record<string, unknown>, key: string, fallback = false): boolean => {
    const raw = meta[key];
    if (typeof raw === 'boolean') {
        return raw;
    }
    if (typeof raw === 'number') {
        return raw !== 0;
    }
    if (typeof raw === 'string') {
        const lowered = raw.toLowerCase();
        if (lowered === 'true' || lowered === '1') {
            return true;
        }
        if (lowered === 'false' || lowered === '0') {
            return false;
        }
    }
    return fallback;
};

const pickUsingWeightPair = <T>(rng: RandomGenerator, items: Array<[T, number]>): T | null => {
    if (items.length === 0) {
        return null;
    }
    let total = 0;
    for (const [, weight] of items) {
        if (weight > 0) {
            total += weight;
        }
    }
    if (total <= 0) {
        return items[0]?.[0] ?? null;
    }
    let cursor = rng.nextFloat1() * total;
    for (const [item, weight] of items) {
        if (weight <= 0) {
            continue;
        }
        cursor -= weight;
        if (cursor <= 0) {
            return item;
        }
    }
    return items[items.length - 1]?.[0] ?? null;
};

const calcNationPower = <TriggerState extends GeneralTriggerState>(generals: General<TriggerState>[]): number => {
    if (generals.length === 0) {
        return 0;
    }
    let warPower = 0;
    let develPower = 0;
    for (const general of generals) {
        const leadership = general.stats.leadership;
        if (leadership >= 40) {
            const coef = general.npcState < 2 ? 1.15 : 1;
            const killCrew =
                resolveNumber(asRecord(general.meta), ['rank_killcrew_person', 'killcrew_person'], 0) + 50_000;
            const deathCrew =
                resolveNumber(asRecord(general.meta), ['rank_deathcrew_person', 'deathcrew_person'], 0) + 50_000;
            warPower += (killCrew / deathCrew) * coef * leadership;
        }
        const base = Math.sqrt(general.stats.intelligence * general.stats.strength) * 2 + leadership / 2;
        develPower += base / 5;
    }
    return warPower + develPower;
};

export class ActionDefinition<
    TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<
    TriggerState,
    RandomAppointmentArgs,
    RandomAppointmentResolveContext<TriggerState>
> {
    public readonly key = 'che_랜덤임관';
    public readonly name = ACTION_NAME;
    private readonly pipeline: GeneralActionPipeline<TriggerState>;

    constructor(env: TurnCommandEnv) {
        this.pipeline = new GeneralActionPipeline(env.generalActionModules ?? []);
    }
    getInheritanceActiveActionAmount(): number {
        return 1;
    }

    parseArgs(_raw: unknown): RandomAppointmentArgs | null {
        return {};
    }

    buildMinConstraints(_ctx: ConstraintContext, _args: RandomAppointmentArgs): Constraint[] {
        return [];
    }

    buildConstraints(_ctx: ConstraintContext, _args: RandomAppointmentArgs): Constraint[] {
        return [beNeutral(), allowJoinAction()];
    }

    resolve(
        context: RandomAppointmentResolveContext<TriggerState>,
        _args: RandomAppointmentArgs
    ): GeneralActionOutcome<TriggerState> {
        const { general, rng, candidateNations } = context;
        const effects: Array<GeneralActionEffect<TriggerState>> = [];

        if (candidateNations.length === 0) {
            effects.push(
                createLogEffect('임관 가능한 국가가 없습니다.', {
                    scope: LogScope.GENERAL,
                    category: LogCategory.ACTION,
                    format: LogFormat.MONTH,
                })
            );
            return { effects, alternative: { commandKey: 'che_인재탐색', args: {} } };
        }

        let picked: CandidateNation<TriggerState> | null;
        if (context.historicalNpcAffinityMode) {
            const totalGenerals = candidateNations.reduce((sum, candidate) => sum + candidate.generalCount, 0);
            let actorAffinity = resolveNumber(asRecord(general.meta), ['affinity'], 0);
            let bestScore = 1 << 30;
            picked = null;
            for (const candidate of candidateNations) {
                // 레거시 구현은 actorAffinity를 매 후보마다 원래 값으로 되돌리지 않는다.
                actorAffinity = Math.abs(actorAffinity - candidate.monarchAffinity);
                actorAffinity = Math.min(actorAffinity, Math.abs(actorAffinity - 150));
                const score =
                    Math.log2(actorAffinity + 1) +
                    rng.nextFloat1() +
                    Math.sqrt(candidate.generalCount / Math.max(totalGenerals, 1));
                if (score < bestScore) {
                    bestScore = score;
                    picked = candidate;
                }
            }
        } else {
            const weightedCandidates = candidateNations.map((candidate) => {
                let power = calcNationPower(candidate.generals);
                if (general.npcState < 2 && candidate.nation.name.startsWith('ⓤ')) {
                    power *= 100;
                }
                const normalized = power > 0 ? power : 1;
                const weight = Math.pow(1 / normalized, 3);
                return [candidate, weight] as [CandidateNation<TriggerState>, number];
            });
            picked = pickUsingWeightPair(rng, weightedCandidates);
        }
        if (!picked) {
            effects.push(
                createLogEffect('임관 가능한 국가가 없습니다.', {
                    scope: LogScope.GENERAL,
                    category: LogCategory.ACTION,
                    format: LogFormat.MONTH,
                })
            );
            return { effects, alternative: { commandKey: 'che_인재탐색', args: {} } };
        }

        const destNation = picked.nation;
        const destNationName = destNation.name;
        const destCityId = picked.monarchCityId ?? destNation.capitalCityId ?? general.cityId;
        const generalName = general.name;
        const josaYi = JosaUtil.pick(generalName, '이');
        const talk = TALK_LIST[rng.nextInt(0, TALK_LIST.length)] ?? TALK_LIST[0]!;

        const meta = {
            ...general.meta,
            belong: 1,
            officer_city: 0,
            officerCity: 0,
        };

        const expGain =
            context.initialNationGenLimit > 0 && picked.generalCount < context.initialNationGenLimit ? 700 : 100;

        effects.push(
            createGeneralPatchEffect<TriggerState>({
                nationId: destNation.id,
                officerLevel: 1,
                cityId: destCityId,
                experience: general.experience + this.pipeline.onCalcStat(context, 'experience', expGain),
                meta,
            }),
            createNationPatchEffect(
                {
                    meta: {
                        ...destNation.meta,
                        gennum: picked.generalCount + 1,
                    },
                },
                destNation.id
            ),
            createLogEffect(`<D>${destNationName}</>에 랜덤 임관했습니다.`, {
                scope: LogScope.GENERAL,
                category: LogCategory.ACTION,
                format: LogFormat.MONTH,
            }),
            createLogEffect(`<D><b>${destNationName}</b></>에 랜덤 임관`, {
                scope: LogScope.GENERAL,
                category: LogCategory.HISTORY,
                format: LogFormat.YEAR_MONTH,
            }),
            createLogEffect(
                `<Y>${generalName}</>${josaYi} ${talk} <D><b>${destNationName}</b></>에 <S>임관</>했습니다.`,
                {
                    scope: LogScope.SYSTEM,
                    category: LogCategory.SUMMARY,
                    format: LogFormat.MONTH,
                }
            )
        );

        tryApplyUniqueLottery(context, {
            acquireType: '랜덤 임관',
            reason: ACTION_NAME,
            nationName: destNationName,
        });

        return { effects };
    }
}

export const actionContextBuilder: ActionContextBuilder = (base, options) => {
    const worldRef = options.worldRef;
    const startYear = resolveStartYear(options.world, options.scenarioMeta);
    const relYear = Math.max(options.world.currentYear - startYear, 0);
    if (!worldRef) {
        return {
            ...base,
            candidateNations: [],
            relYear,
            initialNationGenLimit: 0,
            historicalNpcAffinityMode: false,
        };
    }

    const constValues = asRecord(options.scenarioConfig.const);
    const worldMeta = asRecord(options.world.meta);
    const initialNationGenLimit = resolveNumber(
        constValues,
        ['initialNationGenLimit'],
        LEGACY_INITIAL_NATION_GEN_LIMIT
    );
    const defaultMaxGeneral = resolveNumber(
        constValues,
        ['defaultMaxGeneral', 'maxGeneral'],
        LEGACY_DEFAULT_MAX_GENERAL
    );
    const genLimit = relYear < 3 && initialNationGenLimit > 0 ? initialNationGenLimit : defaultMaxGeneral;

    const generals = worldRef.listGenerals();
    const generalsByNation = new Map<number, General[]>();
    const monarchCityByNation = new Map<number, number>();

    for (const general of generals) {
        if (general.nationId <= 0 || ![0, 1, 2, 3, 6].includes(general.npcState)) {
            continue;
        }
        const list = generalsByNation.get(general.nationId) ?? [];
        list.push(general as General);
        generalsByNation.set(general.nationId, list);
        if (general.officerLevel === 12) {
            monarchCityByNation.set(general.nationId, general.cityId);
        }
    }

    // Ref's GROUP BY result is observed in ascending nation id order. The
    // weighted draw is order-sensitive, while newly founded Core nations can
    // remain in action/creation order, so make the legacy candidate order
    // explicit before consuming the command RNG.
    const nations = [...worldRef.listNations()].sort((left, right) => left.id - right.id);
    const candidateNations: Array<CandidateNation> = [];

    for (const nation of nations) {
        if (nation.id <= 0) {
            continue;
        }
        const meta = asRecord(nation.meta);
        const isScoutBlocked = readMetaBool(meta, 'scout', readMetaBool(meta, 'blockScout', false));
        if (isScoutBlocked) {
            continue;
        }
        const nationGenerals = generalsByNation.get(nation.id) ?? [];
        if (nationGenerals.length === 0) {
            continue;
        }
        const generalCount = resolveNumber(meta, ['gennum'], nationGenerals.length);
        if (typeof genLimit === 'number' && genLimit > 0 && generalCount >= genLimit) {
            continue;
        }
        let monarchCityId = monarchCityByNation.get(nation.id) ?? null;
        if (!monarchCityId && nation.chiefGeneralId) {
            const chief = worldRef.getGeneralById(nation.chiefGeneralId);
            monarchCityId = chief?.cityId ?? null;
        }
        if (!monarchCityId && nation.capitalCityId) {
            monarchCityId = nation.capitalCityId;
        }
        if (!monarchCityId) {
            continue;
        }
        const chief =
            nationGenerals.find((general) => general.officerLevel === 12) ??
            (nation.chiefGeneralId ? worldRef.getGeneralById(nation.chiefGeneralId) : null);
        candidateNations.push({
            nation,
            generals: nationGenerals,
            generalCount,
            monarchCityId,
            monarchAffinity: chief ? resolveNumber(asRecord(chief.meta), ['affinity'], 0) : 0,
        });
    }

    const scenarioId = resolveNumber(worldMeta, ['scenarioId', 'scenario'], 0);
    const fiction = resolveNumber(asRecord(options.scenarioMeta), ['fiction'], 0);
    return {
        ...base,
        candidateNations,
        relYear,
        initialNationGenLimit,
        historicalNpcAffinityMode:
            base.general.npcState >= 2 && fiction === 0 && scenarioId >= 1000 && scenarioId < 2000,
    };
};

export const commandSpec: GeneralTurnCommandSpec = {
    key: 'che_랜덤임관',
    category: '전략',
    reqArg: false,
    createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env),
};
