import { asNumber, asRecord } from '@sammo-ts/common';
import {
    ActionLogger,
    LogFormat,
    createIncomeActionContext,
    getBill,
    getGoldIncome,
    getOutcome,
    getRiceIncome,
    getWallIncome,
    readLegacyCityTrust,
    type CityIncomeSource,
    type Nation,
    type NationIncomeContext,
    type NationTraitModule,
    type TriggerNationalIncomeType,
} from '@sammo-ts/logic';

import type { ScenarioConfig } from '@sammo-ts/logic';

import type { InMemoryTurnWorld, TurnCalendarHandler, TurnCalendarContext } from './inMemoryWorld.js';
import { resolveAppliedNationRate } from './nationTaxRate.js';
import type { TurnGeneral } from './types.js';

const DEFAULT_BASE_GOLD = 0;
const DEFAULT_BASE_RICE = 2_000;

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 resolveNationBill = (nation: Nation): number => asNumber(nation.meta.bill, 100);

const resolveOfficerCity = (meta: Record<string, unknown>): number => {
    const camel = asNumber(meta.officerCity, 0);
    if (camel > 0) {
        return camel;
    }
    return asNumber(meta.officer_city, 0);
};

export const resolveLegacyIncomeCityTrust = (trust: number): number => readLegacyCityTrust(trust);

const resolveCityTrust = (meta: Record<string, unknown>): number => {
    const trust = asNumber(meta.trust, 50);
    // Income is calculated in PHP after PDO exposes MariaDB FLOAT using a
    // six-significant-digit decimal representation.
    return resolveLegacyIncomeCityTrust(trust);
};

const toIncomeCity = (city: ReturnType<InMemoryTurnWorld['listCities']>[number]): CityIncomeSource => ({
    id: city.id,
    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: resolveCityTrust(asRecord(city.meta)),
    supplyState: city.supplyState,
    defence: city.defence,
    defenceMax: city.defenceMax,
    wall: city.wall,
    wallMax: city.wallMax,
    meta: asRecord(city.meta),
});

const buildNationIncomeContext = (nation: Nation, trait: NationTraitModule | null): NationIncomeContext => {
    const actionContext = createIncomeActionContext(nation);
    const modifyIncome = trait?.onCalcNationalIncome
        ? (type: TriggerNationalIncomeType, amount: number) => trait.onCalcNationalIncome!(actionContext, type, amount)
        : undefined;
    return {
        rate: resolveAppliedNationRate(nation.meta),
        modifyIncome,
    };
};

const buildOfficerCountMap = (generals: TurnGeneral[]): Map<number, number> => {
    const officerCntByCity = new Map<number, number>();
    for (const general of generals) {
        if (general.officerLevel < 2 || general.officerLevel > 4) {
            continue;
        }
        const officerCity = resolveOfficerCity(asRecord(general.meta));
        if (!officerCity || general.cityId !== officerCity) {
            continue;
        }
        officerCntByCity.set(officerCity, (officerCntByCity.get(officerCity) ?? 0) + 1);
    }
    return officerCntByCity;
};

const pushLogs = (world: InMemoryTurnWorld, logs: ReturnType<ActionLogger['flush']>): void => {
    for (const log of logs) {
        world.pushLog(log);
    }
};

const applyIncomeOutcome = (
    current: number,
    income: number,
    outcome: number,
    baseResource: number,
    originOutcome: number
): { next: number; ratio: number; realOutcome: number } => {
    let next = current + income;
    let realOutcome: number;
    if (next < baseResource) {
        realOutcome = 0;
        next = baseResource;
    } else if (next - baseResource < outcome) {
        realOutcome = next - baseResource;
        next = baseResource;
    } else {
        realOutcome = outcome;
        next -= realOutcome;
    }

    const ratio = originOutcome > 0 ? realOutcome / originOutcome : 0;
    return { next: Math.max(next, baseResource), ratio, realOutcome };
};

const processIncomeForNation = (
    world: InMemoryTurnWorld,
    nation: Nation,
    generals: TurnGeneral[],
    cities: ReturnType<InMemoryTurnWorld['listCities']>,
    officerCounts: Map<number, number>,
    traitMap: Map<string, NationTraitModule>,
    type: 'gold' | 'rice',
    baseResource: number
): void => {
    const nationCities = cities.filter((city) => city.nationId === nation.id).map(toIncomeCity);
    const nationGenerals = generals.filter((general) => general.nationId === nation.id && general.npcState !== 5);
    const trait = traitMap.get(nation.typeCode) ?? null;
    const incomeContext = buildNationIncomeContext(nation, trait);

    const income =
        type === 'gold'
            ? getGoldIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level)
            : getRiceIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level) +
              getWallIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level);

    // REF-COMPAT:BEGIN ref-income-preflush-fraction
    // Ref calculates the payout ratio from the pre-persistence income value.
    // Half-unit income (for example 943.5) is therefore not rounded before
    // salaries are distributed, even though the integer nation column is
    // rounded when the final state is flushed to MariaDB.
    const incomeValue = income;
    // REF-COMPAT:END ref-income-preflush-fraction
    const originOutcome = getOutcome(100, nationGenerals);
    const bill = resolveNationBill(nation);
    const outcome = Math.round((bill / 100) * originOutcome);
    const current = type === 'gold' ? nation.gold : nation.rice;

    const { next, ratio } = applyIncomeOutcome(current, incomeValue, outcome, baseResource, originOutcome);
    const nextMeta = {
        ...nation.meta,
        [`prev_income_${type}`]: incomeValue,
    };

    if (type === 'gold') {
        world.updateNation(nation.id, { gold: next, meta: nextMeta });
    } else {
        world.updateNation(nation.id, { rice: next, meta: nextMeta });
    }

    const incomeText = incomeValue.toLocaleString();
    const incomeLog =
        type === 'gold' ? `이번 수입은 금 <C>${incomeText}</>입니다.` : `이번 수입은 쌀 <C>${incomeText}</>입니다.`;
    for (const general of nationGenerals) {
        const pay = Math.round(getBill(general.dedication) * ratio);
        if (
            process.env.SEED_PARITY_MONTHLY_RESOURCE_TRACE === '1' &&
            (process.env.AI_TRACE_GENERAL_IDS ?? '').split(',').includes(String(general.id))
        ) {
            process.stdout.write(
                `MONTHLY_RESOURCE_CORE ${JSON.stringify({ action: 'ProcessIncome', type, generalId: general.id, current: general[type], pay, ratio, originOutcome })}\n`
            );
        }
        if (type === 'gold') {
            world.updateGeneral(general.id, { gold: general.gold + pay });
        } else {
            world.updateGeneral(general.id, { rice: general.rice + pay });
        }

        const logger = new ActionLogger({ generalId: general.id, nationId: nation.id });
        if (general.officerLevel > 4) {
            logger.pushGeneralActionLog(incomeLog, LogFormat.PLAIN);
        }
        const payText = pay.toLocaleString();
        const payLog =
            type === 'gold'
                ? `봉급으로 금 <C>${payText}</>을 받았습니다.`
                : `봉급으로 쌀 <C>${payText}</>을 받았습니다.`;
        logger.pushGeneralActionLog(payLog, LogFormat.PLAIN);
        pushLogs(world, logger.flush());
    }
};

export interface IncomeHandler extends TurnCalendarHandler {
    runResource(resource: 'gold' | 'rice'): void;
}

export const createIncomeHandler = (options: {
    getWorld: () => InMemoryTurnWorld | null;
    scenarioConfig: ScenarioConfig;
    nationTraits: Map<string, NationTraitModule>;
}): IncomeHandler => {
    const constValues = asRecord(options.scenarioConfig.const);
    const baseGold = resolveNumber(constValues, ['baseGold', 'basegold'], DEFAULT_BASE_GOLD);
    const baseRice = resolveNumber(constValues, ['baseRice', 'baserice'], DEFAULT_BASE_RICE);

    const runResource = (type: 'gold' | 'rice'): void => {
        const world = options.getWorld();
        if (!world) {
            return;
        }
        const nations = world.listNations();
        const generals = world.listGenerals();
        const cities = world.listCities();

        const byNation: Map<number, TurnGeneral[]> = new Map();
        for (const general of generals) {
            const bucket = byNation.get(general.nationId) ?? [];
            bucket.push(general);
            byNation.set(general.nationId, bucket);
        }

        for (const nation of nations) {
            if (nation.id <= 0) {
                continue;
            }
            const nationGenerals = byNation.get(nation.id) ?? [];
            const officerCounts = buildOfficerCountMap(nationGenerals);
            processIncomeForNation(
                world,
                nation,
                nationGenerals,
                cities,
                officerCounts,
                options.nationTraits,
                type,
                type === 'gold' ? baseGold : baseRice
            );
        }

        const logger = new ActionLogger();
        if (type === 'gold') {
            logger.pushGlobalHistoryLog('<W><b>【지급】</b></>봄이 되어 봉록에 따라 자금이 지급됩니다.');
        } else {
            logger.pushGlobalHistoryLog('<W><b>【지급】</b></>가을이 되어 봉록에 따라 군량이 지급됩니다.');
        }
        pushLogs(world, logger.flush());
    };

    const handler: IncomeHandler = {
        runResource,
        onMonthChanged: (context: TurnCalendarContext) => {
            if (context.currentMonth === 1) {
                runResource('gold');
            } else if (context.currentMonth === 7) {
                runResource('rice');
            }
        },
    };

    return handler;
};
