import { asRecord } from '@sammo-ts/common';
import { createGamePostgresConnector, GamePrisma, type RedisConnector } from '@sammo-ts/infra';
import { buildNeutralResourceAuctionPlan } from '@sammo-ts/logic';

import type { TurnCalendarHandler } from '../turn/inMemoryWorld.js';
import type { InMemoryTurnWorld } from '../turn/inMemoryWorld.js';

interface NeutralAuctionCountRow {
    type: 'BUY_RICE' | 'SELL_RICE';
    count: bigint | number;
}

interface TournamentState {
    stage?: unknown;
}

const readFiniteNumber = (value: unknown, fallback: number): number => {
    if (typeof value === 'number' && Number.isFinite(value)) {
        return value;
    }
    if (typeof value === 'string') {
        const parsed = Number(value);
        if (Number.isFinite(parsed)) {
            return parsed;
        }
    }
    return fallback;
};

const average = (values: number[]): number => {
    if (values.length === 0) {
        return 0;
    }
    return values.reduce((sum, value) => sum + value, 0) / values.length;
};

const parseTournamentState = (raw: string | null): TournamentState | null => {
    if (!raw) {
        return null;
    }
    try {
        const parsed: unknown = JSON.parse(raw);
        return asRecord(parsed);
    } catch {
        return null;
    }
};

const isTournamentActive = async (
    profileName: string,
    redis: RedisConnector['client'] | null | undefined
): Promise<boolean> => {
    if (!redis) {
        return false;
    }
    const state = parseTournamentState(await redis.get(`sammo:${profileName}:tournament:state`));
    return readFiniteNumber(state?.stage, 0) > 0;
};

export interface NeutralAuctionRegistrar {
    handler: TurnCalendarHandler;
    close(): Promise<void>;
}

export const createNeutralAuctionRegistrar = async (options: {
    databaseUrl: string;
    profileName: string;
    getWorld: () => InMemoryTurnWorld | null;
    getRedisClient: () => RedisConnector['client'] | null | undefined;
    getWorldConfig: () => Record<string, unknown> | null | undefined;
    now?: () => Date;
    loadNeutralAuctionCounts?: () => Promise<NeutralAuctionCountRow[]>;
    loadTournamentActive?: () => Promise<boolean>;
    getNationPowerRollCount?: () => number;
    getTournamentRollConsumed?: () => boolean;
}): Promise<NeutralAuctionRegistrar> => {
    const connector = options.loadNeutralAuctionCounts
        ? null
        : createGamePostgresConnector({ url: options.databaseUrl });
    await connector?.connect();
    const loadNeutralAuctionCounts =
        options.loadNeutralAuctionCounts ??
        (() =>
            connector!.prisma.$queryRaw<NeutralAuctionCountRow[]>(
                GamePrisma.sql`
                    SELECT type, count(*) AS count
                    FROM auction
                    WHERE host_general_id = 0
                      AND type IN ('BUY_RICE'::"auction_type", 'SELL_RICE'::"auction_type")
                    GROUP BY type
                `
            ));

    const handler: TurnCalendarHandler = {
        onMonthChanged: async (context) => {
            const world = options.getWorld();
            if (!world) {
                return;
            }
            const state = world.getState();
            const hiddenSeed =
                typeof state.meta.hiddenSeed === 'string' || typeof state.meta.hiddenSeed === 'number'
                    ? state.meta.hiddenSeed
                    : state.id;
            const eligibleGenerals = world.listGenerals().filter((general) => general.npcState < 2);
            const counts = await loadNeutralAuctionCounts();
            const countByType = new Map(counts.map((row) => [row.type, Number(row.count)]));
            for (const pending of world.peekDirtyState().pendingNeutralAuctions) {
                countByType.set(pending.type, (countByType.get(pending.type) ?? 0) + 1);
            }
            const worldConfig = asRecord(options.getWorldConfig() ?? {});
            const consumeTournamentRoll =
                options.getTournamentRollConsumed?.() ??
                (worldConfig.tournamentTrig === true &&
                    !(await (options.loadTournamentActive
                        ? options.loadTournamentActive()
                        : isTournamentActive(options.profileName, options.getRedisClient()))));
            const plans = buildNeutralResourceAuctionPlan({
                hiddenSeed,
                seedYear: context.previousYear,
                seedMonth: context.previousMonth,
                nationCount: options.getNationPowerRollCount?.() ?? world.listNations().length,
                consumeTournamentRoll,
                averageGold: average(eligibleGenerals.map((general) => general.gold)),
                averageRice: average(eligibleGenerals.map((general) => general.rice)),
                buyRiceAuctionCount: countByType.get('BUY_RICE') ?? 0,
                sellRiceAuctionCount: countByType.get('SELL_RICE') ?? 0,
            });

            const registrationKey = `${context.currentYear}-${String(context.currentMonth).padStart(2, '0')}`;
            world.updateWorldMeta({ neutralAuctionRegistrationKey: registrationKey });
            const turnMinutes = Math.max(1, Math.round(state.tickSeconds / 60));
            for (const plan of plans) {
                const openedAt = options.now?.() ?? new Date();
                const hostResourceName = plan.auctionType === 'BUY_RICE' ? '쌀' : '금';
                world.queueNeutralAuction({
                    registrationKey,
                    type: plan.auctionType,
                    targetCode: String(plan.amount),
                    hostGeneralId: 0,
                    hostName: '상인',
                    detail: {
                        title: `${hostResourceName} ${plan.amount} 경매`,
                        hostName: '상인',
                        amount: plan.amount,
                        isReverse: false,
                        startBidAmount: plan.startBidAmount,
                        finishBidAmount: plan.finishBidAmount,
                        neutralRegistrationKey: registrationKey,
                        seedYear: context.previousYear,
                        seedMonth: context.previousMonth,
                        closeTurnCnt: plan.closeTurnCnt,
                    },
                    closeAt: new Date(openedAt.getTime() + plan.closeTurnCnt * turnMinutes * 60_000),
                });
            }
        },
    };

    return {
        handler,
        close: async () => {
            await connector?.disconnect();
        },
    };
};
