import { createTournamentRng, type TurnDaemonCommandResult } from '@sammo-ts/common';
import { resolveTournamentBattle } from '@sammo-ts/logic';
import {
    createGamePostgresConnector,
    createRedisConnector,
    resolvePostgresConfigFromEnv,
    resolveRedisConfigFromEnv,
    type GamePrismaClient,
} from '@sammo-ts/infra';

import { resolveGameApiConfigFromEnv } from '../config.js';
import { DatabaseTurnDaemonTransport } from '../daemon/databaseTransport.js';
import { createBestEffortResourceCloser } from '../services/bestEffortResourceCloser.js';
import { loadCurrentGameTime } from '../services/gameClock.js';
import { createPollingWorkerControl, waitForWorkerPoll } from '../services/pollingWorkerLifecycle.js';
import type { TurnDaemonTransport } from '../daemon/transport.js';
import { buildTournamentKeys } from './keys.js';
import { TournamentStore } from './store.js';
import type { TournamentMatchEntry, TournamentState } from './types.js';
import {
    applyGroupMatch,
    assignGroupSlots,
    buildBettingPayouts,
    buildFinal16MatchesFromGroups,
    buildInitialMatches,
    buildNextMatches,
    buildTournamentRewardPayload,
    fillParticipants,
    isBattleStage,
    isPreBattleStage,
    nextStage,
    pickFinalists,
    resolveBettingCloseAt,
    resolveGroupPair,
    resolveNextAt,
    seedNpcBets,
    sortByRanking,
    type TournamentMatchOutcome,
    type TournamentPrismaClient,
} from './workerHelpers.js';

const persistLatestGroupFightLogs = async (
    store: TournamentStore,
    stage: 2 | 4,
    outcomes: TournamentMatchOutcome[]
): Promise<void> => {
    if (outcomes.length === 0) {
        return;
    }

    const matches = await store.getMatches();
    const replacedGroupIds = new Set(outcomes.map((outcome) => outcome.groupId));
    const retained = matches.filter(
        (match) => match.stage !== stage || match.groupId === undefined || !replacedGroupIds.has(match.groupId)
    );
    const latest = outcomes.map((outcome): TournamentMatchEntry => ({
        id: stage * 100 + outcome.groupId + 1,
        stage,
        roundIndex: outcome.groupId,
        groupId: outcome.groupId,
        attackerId: outcome.attackerId,
        defenderId: outcome.defenderId,
        winnerId: outcome.winnerId,
        log: outcome.log,
        logEntries: outcome.logEntries,
        lastEnergy: outcome.lastEnergy,
    }));
    await store.setMatches(retained.concat(latest));
};

export const applyBattle = async (
    store: TournamentStore,
    state: TournamentState,
    baseSeed: string,
    daemonTransport?: TurnDaemonTransport
): Promise<TournamentState> => {
    const matches = await store.getMatches();
    const participants = await store.getParticipants();

    const pending = matches.filter((match) => match.stage === state.stage && !match.winnerId);
    if (pending.length === 0) {
        const next = nextStage(state.stage);
        if (next === 0) {
            const finalMatch = matches.find((match) => match.stage === state.stage && match.winnerId);
            const finished: TournamentState = {
                ...state,
                stage: 0,
                phase: 0,
                auto: false,
                winnerId: finalMatch?.winnerId,
                nextAt: resolveNextAt(state),
            };
            await store.setState(finished);
            return finished;
        }

        const nextMatches = buildNextMatches(state.stage, matches);
        await store.setMatches(matches.concat(nextMatches));

        const nextState: TournamentState = {
            ...state,
            stage: next,
            phase: 0,
            nextAt: resolveNextAt(state),
        };
        await store.setState(nextState);
        return nextState;
    }

    const target = pending[state.phase] ?? pending[0];
    if (!target) {
        throw new Error('토너먼트 매치가 없습니다.');
    }

    const attacker = participants.find((entry) => entry.id === target.attackerId);
    const defender = participants.find((entry) => entry.id === target.defenderId);
    if (!attacker || !defender) {
        throw new Error('토너먼트 참가자 정보를 찾을 수 없습니다.');
    }

    const result = resolveTournamentBattle({
        type: state.type,
        battleType: 1,
        attacker: {
            id: attacker.id,
            name: attacker.name,
            stats: {
                leadership: attacker.leadership,
                strength: attacker.strength,
                intel: attacker.intel,
            },
            level: attacker.level,
        },
        defender: {
            id: defender.id,
            name: defender.name,
            stats: {
                leadership: defender.leadership,
                strength: defender.strength,
                intel: defender.intel,
            },
            level: defender.level,
        },
        context: {
            openYear: state.openYear,
            openMonth: state.openMonth,
            stage: state.stage,
            phase: state.phase,
            matchIndex: target.id,
        },
        baseSeed,
    });

    const lastLogEntry = result.logEntries[result.logEntries.length - 1] ?? null;

    const updatedMatch: TournamentMatchEntry = {
        ...target,
        winnerId: result.winnerId ?? undefined,
        log: result.log,
        logEntries: result.logEntries,
        lastEnergy: lastLogEntry
            ? { attacker: lastLogEntry.attackerEnergy, defender: lastLogEntry.defenderEnergy }
            : undefined,
    };

    const nextMatches = matches.map((entry) => (entry.id === target.id ? updatedMatch : entry));
    await store.setMatches(nextMatches);

    if (daemonTransport && attacker.id > 0 && defender.id > 0) {
        await daemonTransport.sendCommand({
            type: 'tournamentMatchResult',
            tournamentType: state.type,
            attackerId: attacker.id,
            defenderId: defender.id,
            result: result.draw ? 'draw' : result.winnerId === attacker.id ? 'attacker' : 'defender',
        });
    }

    const nextPhase = state.phase + 1;
    const nextState: TournamentState = {
        ...state,
        phase: nextPhase,
        nextAt: resolveNextAt(state),
    };
    await store.setState(nextState);
    return nextState;
};

export const applyPreBattleStage = async (
    store: TournamentStore,
    prisma: TournamentPrismaClient,
    state: TournamentState,
    baseSeed: string,
    daemonTransport: TurnDaemonTransport,
    now: () => number = Date.now
): Promise<TournamentState> => {
    const participants = await store.getParticipants();

    if (state.stage === 1) {
        let nextParticipants = participants;
        if (participants.length < 64) {
            nextParticipants = await fillParticipants({
                prisma,
                state,
                baseSeed,
                current: participants,
                limit: 64,
            });
        }
        const grouped = assignGroupSlots(nextParticipants, 8, 8, 0).map((entry) => ({
            ...entry,
            win: 0,
            draw: 0,
            lose: 0,
            gl: 0,
            seedRank: 0,
            finalRank: 0,
            preliminaryGroupId: entry.groupId,
            preliminaryGroupNo: entry.groupNo,
        }));
        await store.setParticipants(grouped);
        const nextState: TournamentState = {
            ...state,
            stage: 2,
            phase: 0,
            participantsLockedAt: new Date(now()).toISOString(),
            nextAt: resolveNextAt(state),
        };
        await store.setState(nextState);
        return nextState;
    }

    if (state.stage === 2) {
        const pair = resolveGroupPair(2, state.phase);
        if (!pair) {
            throw new Error('예선 매치 구성을 찾을 수 없습니다.');
        }
        let updated = participants.some((entry) => entry.groupId === undefined)
            ? assignGroupSlots(participants, 8, 8, 0)
            : participants;
        const outcomes: TournamentMatchOutcome[] = [];
        for (let groupId = 0; groupId < 8; groupId += 1) {
            const groupEntries = updated.filter((entry) => entry.groupId === groupId);
            const attacker = groupEntries.find((entry) => entry.groupNo === pair[0]);
            const defender = groupEntries.find((entry) => entry.groupNo === pair[1]);
            if (!attacker || !defender) {
                continue;
            }
            const result = applyGroupMatch(updated, attacker, defender, state, baseSeed, groupId);
            updated = result.participants;
            outcomes.push(result.outcome);
        }
        await store.setParticipants(updated);
        await persistLatestGroupFightLogs(store, 2, outcomes);

        if (outcomes.length > 0) {
            await Promise.all(
                outcomes
                    .filter((outcome) => outcome.attackerId > 0 && outcome.defenderId > 0)
                    .map((outcome) =>
                        daemonTransport.sendCommand({
                            type: 'tournamentMatchResult',
                            tournamentType: state.type,
                            attackerId: outcome.attackerId,
                            defenderId: outcome.defenderId,
                            result: outcome.result,
                        })
                    )
            );
        }

        const maxPhase = 55;
        const isComplete = state.phase >= maxPhase;
        if (isComplete) {
            const ranked = updated;
            for (let groupId = 0; groupId < 8; groupId += 1) {
                const groupEntries = ranked.filter((entry) => entry.groupId === groupId);
                const ordered = sortByRanking(groupEntries);
                ordered.forEach((entry, idx) => {
                    const target = ranked.find((item) => item.id === entry.id);
                    if (target) {
                        target.preliminaryGroupId = groupId;
                        target.preliminaryGroupNo = entry.groupNo;
                        target.preliminaryRank = idx + 1;
                        target.preliminaryWin = entry.win ?? 0;
                        target.preliminaryDraw = entry.draw ?? 0;
                        target.preliminaryLose = entry.lose ?? 0;
                        target.preliminaryGl = entry.gl ?? 0;
                        target.seedRank = idx < 4 ? idx + 1 : 0;
                    }
                });
            }
            await store.setParticipants(ranked);
            const nextState: TournamentState = {
                ...state,
                stage: 3,
                phase: 0,
                nextAt: resolveNextAt(state),
            };
            await store.setState(nextState);
            return nextState;
        }

        const nextState: TournamentState = {
            ...state,
            phase: state.phase + 1,
            nextAt: resolveNextAt(state),
        };
        await store.setState(nextState);
        return nextState;
    }

    if (state.stage === 3) {
        const phase = state.phase;
        const groupId = 10 + (phase % 8);
        const groupNo = Math.floor(phase / 8);
        const seedTarget = phase < 8 ? 1 : phase < 16 ? 2 : 3;

        const candidates = participants.filter((entry) => {
            if (entry.groupId !== undefined && entry.groupId >= 10) {
                return false;
            }
            if (seedTarget === 3) {
                return (entry.seedRank ?? 0) > 2;
            }
            return (entry.seedRank ?? 0) === seedTarget;
        });

        if (candidates.length === 0) {
            throw new Error('본선 추첨 후보가 없습니다.');
        }

        const rng = createTournamentRng(baseSeed, {
            openYear: state.openYear,
            openMonth: state.openMonth,
            stage: 3,
            phase,
            matchIndex: groupId,
            participantIndex: 0,
            extraSeed: `selection:${seedTarget}:${candidates.map((c) => c.id).join('-')}`,
        });
        const picked = rng.choice(candidates);

        const nextParticipants = participants.map((entry) => {
            if (entry.id !== picked.id) {
                return entry;
            }
            return {
                ...entry,
                groupId,
                groupNo,
                win: 0,
                draw: 0,
                lose: 0,
                gl: 0,
            };
        });
        await store.setParticipants(nextParticipants);

        if (phase >= 31) {
            const nextState: TournamentState = {
                ...state,
                stage: 4,
                phase: 0,
                nextAt: resolveNextAt(state),
            };
            await store.setState(nextState);
            return nextState;
        }

        const nextState: TournamentState = {
            ...state,
            phase: phase + 1,
            nextAt: resolveNextAt(state),
        };
        await store.setState(nextState);
        return nextState;
    }

    if (state.stage === 4) {
        const pair = resolveGroupPair(4, state.phase);
        if (!pair) {
            throw new Error('본선 매치 구성을 찾을 수 없습니다.');
        }
        let updated = participants;
        const outcomes: TournamentMatchOutcome[] = [];
        for (let groupId = 10; groupId < 18; groupId += 1) {
            const groupEntries = updated.filter((entry) => entry.groupId === groupId);
            const attacker = groupEntries.find((entry) => entry.groupNo === pair[0]);
            const defender = groupEntries.find((entry) => entry.groupNo === pair[1]);
            if (!attacker || !defender) {
                continue;
            }
            const result = applyGroupMatch(updated, attacker, defender, state, baseSeed, groupId);
            updated = result.participants;
            outcomes.push(result.outcome);
        }
        await store.setParticipants(updated);
        await persistLatestGroupFightLogs(store, 4, outcomes);

        if (outcomes.length > 0) {
            await Promise.all(
                outcomes
                    .filter((outcome) => outcome.attackerId > 0 && outcome.defenderId > 0)
                    .map((outcome) =>
                        daemonTransport.sendCommand({
                            type: 'tournamentMatchResult',
                            tournamentType: state.type,
                            attackerId: outcome.attackerId,
                            defenderId: outcome.defenderId,
                            result: outcome.result,
                        })
                    )
            );
        }

        const maxPhase = 5;
        if (state.phase >= maxPhase) {
            for (let groupId = 10; groupId < 18; groupId += 1) {
                const groupEntries = updated.filter((entry) => entry.groupId === groupId);
                const ordered = sortByRanking(groupEntries);
                ordered.slice(0, 2).forEach((entry, idx) => {
                    const target = updated.find((item) => item.id === entry.id);
                    if (target) {
                        target.finalRank = idx + 1;
                    }
                });
            }
            await store.setParticipants(updated);
            const nextState: TournamentState = {
                ...state,
                stage: 5,
                phase: 0,
                nextAt: resolveNextAt(state),
            };
            await store.setState(nextState);
            return nextState;
        }

        const nextState: TournamentState = {
            ...state,
            phase: state.phase + 1,
            nextAt: resolveNextAt(state),
        };
        await store.setState(nextState);
        return nextState;
    }

    if (state.stage === 5) {
        const matches = await store.getMatches();
        if (!matches.some((match) => match.stage >= 7)) {
            const fixedMatches = buildFinal16MatchesFromGroups(participants);
            const participantIds = fixedMatches
                ? fixedMatches.flatMap((entry) => [entry.attackerId, entry.defenderId])
                : pickFinalists(state, participants);
            const initialMatches = fixedMatches ?? buildInitialMatches(state, baseSeed, participantIds);
            await store.setMatches(matches.concat(initialMatches));
        }
        const nextState: TournamentState = {
            ...state,
            stage: 6,
            phase: 0,
            bettingId: state.bettingId ?? now(),
            bettingCloseAt: resolveBettingCloseAt(state),
            nextAt: resolveNextAt(state),
        };
        await store.setState(nextState);
        await seedNpcBets({ prisma, store, state: nextState, baseSeed, daemonTransport });
        return nextState;
    }

    if (state.stage === 6) {
        const bettingCloseAt = state.bettingCloseAt ?? resolveBettingCloseAt(state);
        const bettingCloseMs = new Date(bettingCloseAt).getTime();
        if (Number.isFinite(bettingCloseMs) && bettingCloseMs > now()) {
            const waitingState: TournamentState = {
                ...state,
                bettingCloseAt,
                nextAt: bettingCloseAt,
            };
            await store.setState(waitingState);
            return waitingState;
        }
        const nextState: TournamentState = {
            ...state,
            stage: 7,
            phase: 0,
            bettingCloseAt,
            nextAt: resolveNextAt(state),
        };
        await store.setState(nextState);
        return nextState;
    }

    return state;
};

export const settleTournamentOutcome = async (options: {
    store: TournamentStore;
    daemonTransport: TurnDaemonTransport;
    state: TournamentState;
}): Promise<TournamentState | null> => {
    const { store, daemonTransport, state } = options;
    if (state.stage !== 0 || !state.winnerId) {
        return null;
    }

    let settledState = state;
    let changed = false;

    const requireSuccessfulResult = (
        result: TurnDaemonCommandResult | null,
        expectedType: TurnDaemonCommandResult['type']
    ): void => {
        if (!result) {
            throw new Error(`${expectedType} 명령 응답 시간이 초과되었습니다.`);
        }
        if (result.type !== expectedType) {
            throw new Error(`${expectedType} 명령에 잘못된 응답(${result.type})을 받았습니다.`);
        }
        if (!('ok' in result) || !result.ok) {
            const reason = 'reason' in result ? result.reason : '성공 여부가 없는 응답';
            throw new Error(`${expectedType} 명령이 실패했습니다: ${reason}`);
        }
    };

    if (!settledState.rewardSettled) {
        const matches = await store.getMatches();
        const rewardPayload = buildTournamentRewardPayload(matches);
        const result = await daemonTransport.requestCommand({
            type: 'tournamentReward',
            requestId: `tournament:${state.bettingId ?? `${state.openYear}:${state.openMonth}:${state.type}`}:reward`,
            tournamentType: state.type,
            winnerId: rewardPayload.winnerId,
            runnerUpId: rewardPayload.runnerUpId,
            top16: rewardPayload.top16,
            top8: rewardPayload.top8,
            top4: rewardPayload.top4,
        });
        requireSuccessfulResult(result, 'tournamentReward');
        settledState = {
            ...settledState,
            rewardSettled: true,
        };
        changed = true;
        await store.setState(settledState);
    }

    if (settledState.bettingId && !settledState.bettingSettled) {
        const bettingEntries = await store.getBettingEntries();
        if (bettingEntries.length > 0) {
            const payoutInfo = buildBettingPayouts(settledState.winnerId!, bettingEntries);
            if (payoutInfo.payouts.length > 0) {
                if (payoutInfo.refundAll) {
                    const result = await daemonTransport.requestCommand({
                        type: 'tournamentRefund',
                        requestId: `tournament:${settledState.bettingId}:betting-refund`,
                        bettingId: settledState.bettingId,
                        refunds: payoutInfo.payouts,
                        reason: 'no_winner',
                    });
                    requireSuccessfulResult(result, 'tournamentRefund');
                } else {
                    const result = await daemonTransport.requestCommand({
                        type: 'tournamentBettingPayout',
                        requestId: `tournament:${settledState.bettingId}:betting-payout`,
                        bettingId: settledState.bettingId,
                        tournamentType: settledState.type,
                        payouts: payoutInfo.payouts,
                        reason: 'winner_payout',
                    });
                    requireSuccessfulResult(result, 'tournamentBettingPayout');
                }
            }
        }

        settledState = {
            ...settledState,
            bettingSettled: true,
        };
        changed = true;
        await store.setState(settledState);
    }

    return changed ? settledState : null;
};

const needsSettlement = (state: TournamentState): boolean =>
    state.stage === 0 &&
    Boolean(state.winnerId) &&
    (!state.rewardSettled || (Boolean(state.bettingId) && !state.bettingSettled));

export const processTournamentTick = async (options: {
    store: TournamentStore;
    prisma: GamePrismaClient;
    daemonTransport: TurnDaemonTransport;
    now?: () => number;
}): Promise<TournamentState | null> => {
    const { store, prisma, daemonTransport } = options;
    const now = options.now ?? Date.now;
    let processedState: TournamentState | null = null;

    await store.withMutationLock(async () => {
        const state = await store.getState();
        if (!state || (!state.auto && !needsSettlement(state))) {
            return;
        }
        const nextAt = new Date(state.nextAt).getTime();
        if (state.auto && Number.isFinite(nextAt) && nextAt > now()) {
            return;
        }

        if (needsSettlement(state)) {
            processedState = (await settleTournamentOutcome({ store, daemonTransport, state })) ?? state;
            return;
        }

        const worldState = await prisma.worldState.findFirst();
        const baseSeed = (worldState?.meta as Record<string, unknown> | null)?.hiddenSeed ?? 'tournament';
        let nextState = state;
        if (isBattleStage(state.stage)) {
            nextState = await applyBattle(store, state, String(baseSeed), daemonTransport);
        } else if (isPreBattleStage(state.stage)) {
            nextState = await applyPreBattleStage(store, prisma, state, String(baseSeed), daemonTransport, now);
        }
        processedState =
            (await settleTournamentOutcome({
                store,
                daemonTransport,
                state: nextState,
            })) ?? nextState;
    });

    return processedState;
};

export interface TournamentWorkerOptions {
    signal?: AbortSignal;
}

export const runTournamentWorker = async (options: TournamentWorkerOptions = {}): Promise<void> => {
    const config = resolveGameApiConfigFromEnv();
    const postgres = createGamePostgresConnector(resolvePostgresConfigFromEnv({ schema: config.profile }));
    const redis = createRedisConnector(resolveRedisConfigFromEnv());

    await postgres.connect();
    await redis.connect();

    const store = new TournamentStore(redis.client, buildTournamentKeys(config.profileName));
    const daemonTransport = new DatabaseTurnDaemonTransport(postgres.prisma, config.daemonRequestTimeoutMs);
    const control = createPollingWorkerControl(options.signal);
    const closeResources = createBestEffortResourceCloser([
        { name: 'tournament-worker-redis', run: () => redis.disconnect() },
        { name: 'tournament-worker-postgres', run: () => postgres.disconnect() },
    ]);

    try {
        while (!control.signal.aborted) {
            const state = await store.getState();
            if (!state || (!state.auto && !needsSettlement(state))) {
                await waitForWorkerPoll(control.signal, config.tournamentPollMs);
                continue;
            }

            const nextAt = new Date(state.nextAt).getTime();
            const gameNow = (await loadCurrentGameTime(postgres.prisma)).now.getTime();
            if (state.auto && Number.isFinite(nextAt) && nextAt > gameNow) {
                await waitForWorkerPoll(control.signal, Math.min(config.tournamentPollMs, nextAt - gameNow));
                continue;
            }

            try {
                await processTournamentTick({
                    store,
                    prisma: postgres.prisma,
                    daemonTransport,
                    now: () => gameNow,
                });
            } catch (error) {
                const message = error instanceof Error ? error.message : 'Unknown error';
                const trace = error instanceof Error ? error.stack : undefined;
                const failedAt = new Date().toISOString();
                await postgres.prisma.errorLog.create({
                    data: {
                        category: 'TOURNAMENT',
                        source: 'tournament-worker',
                        message,
                        trace,
                        context: {
                            stage: state.stage,
                            phase: state.phase,
                            bettingId: state.bettingId ?? null,
                            nextAt: state.nextAt,
                        },
                    },
                });
                const currentState = (await store.getState()) ?? state;
                const nextState: TournamentState = {
                    ...currentState,
                    lastError: message,
                    lastErrorAt: failedAt,
                };
                await store.setState(nextState);
            }

            await waitForWorkerPoll(control.signal, config.tournamentPollMs);
        }
    } finally {
        control.dispose();
        await closeResources();
    }
};
