import { TRPCError } from '@trpc/server';
import { GamePrisma } from '@sammo-ts/infra';
import { z } from 'zod';

import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
import { appendInheritanceLog, readInheritancePoint, setInheritancePoint } from '../../services/inheritance.js';
import { getMyGeneral } from '../shared/general.js';

const joinYearMonth = (year: number, month: number): number => year * 12 + month - 1;

const purifySelection = (selection: readonly number[]): number[] =>
    [...new Set(selection)].sort((left, right) => left - right);

const requireUserId = (auth: { user: { id: string } } | null): string => {
    const userId = auth?.user.id;
    if (!userId) {
        throw new TRPCError({ code: 'UNAUTHORIZED' });
    }
    return userId;
};

const loadWorldDate = async (db: Parameters<typeof getMyGeneral>[0]['db']) => {
    const world = await db.worldState.findFirst({
        select: { currentYear: true, currentMonth: true },
    });
    if (!world) {
        throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state not found.' });
    }
    return world;
};

export const bettingRouter = router({
    getList: accessAuthedInputProcedure(z.object({ req: z.literal('bettingNation').optional() }).optional())
        .query(async ({ ctx, input }) => {
            requireUserId(ctx.auth);
            await getMyGeneral(ctx);
            const [world, rows] = await Promise.all([
                loadWorldDate(ctx.db),
                ctx.db.nationBetting.findMany({
                    where: input?.req ? { type: input.req } : undefined,
                    orderBy: { id: 'asc' },
                    include: { bets: { select: { amount: true } } },
                }),
            ]);
            const bettingList = Object.fromEntries(
                rows.map((row) => [
                    row.id,
                    {
                        id: row.id,
                        type: row.type,
                        name: row.name,
                        finished: row.finished,
                        selectCnt: row.selectCount,
                        isExclusive: row.isExclusive,
                        reqInheritancePoint: row.requiresInheritancePoint,
                        openYearMonth: row.openYearMonth,
                        closeYearMonth: row.closeYearMonth,
                        winner: row.winner,
                        totalAmount: row.bets.reduce((sum, bet) => sum + bet.amount, 0),
                    },
                ])
            );
            return {
                result: true,
                bettingList,
                year: world.currentYear,
                month: world.currentMonth,
            };
        }),

    getDetail: authedProcedure
        .input(z.object({ bettingId: z.number().int().positive() }))
        .query(async ({ ctx, input }) => {
            const userId = requireUserId(ctx.auth);
            const general = await getMyGeneral(ctx);
            const [world, betting, remainPoint] = await Promise.all([
                loadWorldDate(ctx.db),
                ctx.db.nationBetting.findUnique({
                    where: { id: input.bettingId },
                    include: { bets: { orderBy: { id: 'asc' } } },
                }),
                ctx.db.inheritancePoint.findUnique({
                    where: { userId_key: { userId, key: 'previous' } },
                    select: { value: true },
                }),
            ]);
            if (!betting) {
                throw new TRPCError({ code: 'NOT_FOUND', message: '해당 베팅이 없습니다' });
            }
            const group = (bets: typeof betting.bets) => {
                const totals = new Map<string, number>();
                for (const bet of bets) {
                    totals.set(bet.selectionKey, (totals.get(bet.selectionKey) ?? 0) + bet.amount);
                }
                return Array.from(totals, ([selection, amount]) => [selection, amount] as const);
            };
            return {
                result: true,
                bettingInfo: {
                    id: betting.id,
                    type: betting.type,
                    name: betting.name,
                    finished: betting.finished,
                    selectCnt: betting.selectCount,
                    isExclusive: betting.isExclusive,
                    reqInheritancePoint: betting.requiresInheritancePoint,
                    openYearMonth: betting.openYearMonth,
                    closeYearMonth: betting.closeYearMonth,
                    candidates: betting.candidates,
                    winner: betting.winner,
                },
                bettingDetail: group(betting.bets),
                myBetting: group(betting.bets.filter((bet) => bet.userId === userId)),
                remainPoint: betting.requiresInheritancePoint ? (remainPoint?.value ?? 0) : general.gold,
                year: world.currentYear,
                month: world.currentMonth,
            };
        }),

    bet: authedProcedure
        .input(
            z.object({
                bettingId: z.number().int().positive(),
                bettingType: z.array(z.number().int().nonnegative()),
                amount: z.number().int().min(10),
            })
        )
        .mutation(async ({ ctx, input }) => {
            const userId = requireUserId(ctx.auth);
            const general = await getMyGeneral(ctx);
            await ctx.db.$queryRaw`
                SELECT id
                FROM nation_betting
                WHERE id = ${input.bettingId}
                FOR UPDATE
            `;
            const betting = await ctx.db.nationBetting.findUnique({ where: { id: input.bettingId } });
            if (!betting) {
                throw new TRPCError({ code: 'NOT_FOUND', message: `해당 베팅이 없습니다: ${input.bettingId}` });
            }
            if (betting.finished) {
                throw new TRPCError({ code: 'BAD_REQUEST', message: '이미 종료된 베팅입니다' });
            }
            const world = await loadWorldDate(ctx.db);
            const yearMonth = joinYearMonth(world.currentYear, world.currentMonth);
            if (betting.closeYearMonth <= yearMonth) {
                throw new TRPCError({ code: 'BAD_REQUEST', message: '이미 마감된 베팅입니다' });
            }
            if (betting.openYearMonth > yearMonth) {
                throw new TRPCError({ code: 'BAD_REQUEST', message: '아직 시작되지 않은 베팅입니다' });
            }

            const selection = purifySelection(input.bettingType);
            if (selection.length !== betting.selectCount || input.bettingType.length !== betting.selectCount) {
                throw new TRPCError({ code: 'BAD_REQUEST', message: '필요한 선택 수를 채우지 못했습니다.' });
            }
            const candidates = Array.isArray(betting.candidates) ? betting.candidates : [];
            if (selection.some((index) => index >= candidates.length)) {
                throw new TRPCError({ code: 'BAD_REQUEST', message: '올바른 후보가 아닙니다.' });
            }
            const selectionKey = JSON.stringify(selection);
            const totals = await ctx.db.$queryRaw<Array<{ total: number | null }>>(
                GamePrisma.sql`
                    SELECT SUM(amount)::float8 AS total
                    FROM nation_bet
                    WHERE betting_id = ${input.bettingId}
                        AND user_id = ${userId}
                `
            );
            const previousBetAmount = totals[0]?.total ?? 0;
            if (previousBetAmount + input.amount > 1_000) {
                throw new TRPCError({
                    code: 'BAD_REQUEST',
                    message: `${1_000 - previousBetAmount}${betting.requiresInheritancePoint ? '유산포인트' : '금'}까지만 베팅 가능합니다.`,
                });
            }

            if (betting.requiresInheritancePoint) {
                const remainingPoint = await readInheritancePoint(ctx.db, userId, 'previous');
                if (remainingPoint < input.amount) {
                    throw new TRPCError({ code: 'BAD_REQUEST', message: '유산포인트가 충분하지 않습니다.' });
                }
                await setInheritancePoint(ctx.db, userId, 'previous', remainingPoint - input.amount);
                await appendInheritanceLog(
                    ctx.db,
                    userId,
                    world.currentYear,
                    world.currentMonth,
                    `${input.amount} 포인트를 베팅에 사용`
                );
                await ctx.db.rankData.upsert({
                    where: {
                        generalId_type: {
                            generalId: general.id,
                            type: 'inherit_spent_dyn',
                        },
                    },
                    update: { value: { increment: input.amount } },
                    create: {
                        generalId: general.id,
                        nationId: general.nationId,
                        type: 'inherit_spent_dyn',
                        value: input.amount,
                    },
                });
            } else {
                throw new TRPCError({
                    code: 'NOT_IMPLEMENTED',
                    message: 'Nation betting currently requires inheritance points.',
                });
            }

            await ctx.db.nationBet.upsert({
                where: {
                    bettingId_userId_selectionKey: {
                        bettingId: input.bettingId,
                        userId,
                        selectionKey,
                    },
                },
                update: { amount: { increment: input.amount }, generalId: general.id, selection },
                create: {
                    bettingId: input.bettingId,
                    generalId: general.id,
                    userId,
                    selection,
                    selectionKey,
                    amount: input.amount,
                },
            });
            ctx.changeJournal?.mark('general.content', general.id);
            ctx.changeJournal?.mark('betting');
            return { result: true };
        }),
});
