import { randomUUID } from 'node:crypto';

import { createGamePostgresConnector, GamePrisma, type GamePrismaClient } from '@sammo-ts/infra';
import { isItemKey, ItemLoader, type MessageDraft } from '@sammo-ts/logic';

import type { TurnDaemonCommand, TurnDaemonCommandResult } from '../lifecycle/types.js';
import type { InMemoryTurnWorld } from '../turn/inMemoryWorld.js';
import type { TurnGeneral } from '../turn/types.js';

export interface AuctionBidder {
    bid(
        command: Extract<TurnDaemonCommand, { type: 'auctionBid' }>,
        db?: GamePrisma.TransactionClient
    ): Promise<TurnDaemonCommandResult>;
    close(): Promise<void>;
}

type AuctionType = 'BUY_RICE' | 'SELL_RICE' | 'UNIQUE_ITEM';

type AuctionStatus = 'OPEN' | 'FINALIZING' | 'FINISHED' | 'CANCELED';

const COEFF_EXTENSION_MINUTES_PER_BID = 1 / 6;
const MIN_EXTENSION_MINUTES_PER_BID = 1;
export const MIN_AUCTION_REMAINING_RESOURCE = 1_000;

interface AuctionRow {
    id: number;
    type: AuctionType;
    targetCode: string | null;
    hostGeneralId: number;
    detail: unknown;
    status: AuctionStatus;
    closeAt: Date;
    closeTick: bigint | null;
    latestEventId: string;
}

interface AuctionBidRow {
    id: number;
    generalId: number;
    amount: number;
    meta: unknown;
}

interface AuctionDetail {
    title?: string;
    isReverse?: boolean;
    startBidAmount?: number;
    finishBidAmount?: number | null;
    availableLatestBidCloseDate?: string | null;
}

export const hasEnoughResourceForAuctionBid = (current: number, additionalBid: number): boolean =>
    Number.isFinite(current) &&
    Number.isFinite(additionalBid) &&
    additionalBid > 0 &&
    current >= additionalBid + MIN_AUCTION_REMAINING_RESOURCE;

export const hasAuctionClosePassed = (
    auction: { closeAt: Date; closeTick: bigint | null },
    now: Date,
    nowTick: number | null
): boolean =>
    auction.closeTick !== null && nowTick !== null
        ? auction.closeTick < BigInt(nowTick)
        : auction.closeAt.getTime() < now.getTime();

export const resolveAuctionBidTiming = (
    world: Pick<InMemoryTurnWorld, 'dateToGameTick' | 'gameTickToDate'>,
    processingNow: Date,
    acceptedGameTick?: number
): { bidAt: Date; bidTick: number } =>
    acceptedGameTick === undefined
        ? { bidAt: processingNow, bidTick: world.dateToGameTick(processingNow) }
        : { bidAt: world.gameTickToDate(acceptedGameTick), bidTick: acceptedGameTick };

export const hasAuctionBidClosePassed = (
    auction: { closeAt: Date; closeTick: bigint | null },
    world: Pick<InMemoryTurnWorld, 'dateToGameTick' | 'gameTickToDate'>,
    processingNow: Date,
    acceptedGameTick?: number
): boolean => {
    const { bidAt, bidTick } = resolveAuctionBidTiming(world, processingNow, acceptedGameTick);
    return hasAuctionClosePassed(auction, bidAt, bidTick);
};

export const buildAuctionOutbidRefundMessage = (options: {
    auctionId: number;
    title?: string;
    bidder: TurnGeneral;
    nation?: { name: string; color: string } | null;
    time: Date;
}): MessageDraft => ({
    msgType: 'private',
    src: {
        generalId: 0,
        generalName: '',
        nationId: 0,
        nationName: 'System',
        color: '#000000',
        icon: '',
    },
    dest: {
        generalId: options.bidder.id,
        generalName: options.bidder.name,
        nationId: options.bidder.nationId,
        nationName: options.nation?.name ?? '재야',
        color: options.nation?.color ?? '#000000',
        icon: options.bidder.picture ?? '',
    },
    text: `${options.auctionId}번 ${options.title ?? '경매'}에 상회입찰자가 나타났습니다.`,
    time: new Date(options.time.getTime()),
    validUntil: new Date('9999-12-31T00:00:00.000Z'),
    option: {},
    sendDestOnly: true,
});

const parseDetail = (detail: unknown): AuctionDetail => {
    if (!detail || typeof detail !== 'object') {
        return {};
    }
    return detail as AuctionDetail;
};

const toFiniteNumber = (value: unknown): number => {
    const parsed = typeof value === 'string' ? Number(value) : value;
    return typeof parsed === 'number' && Number.isFinite(parsed) ? parsed : 0;
};

const readRankTrackedAmount = (bid: AuctionBidRow | null): number => {
    if (!bid?.meta || typeof bid.meta !== 'object' || Array.isArray(bid.meta)) return 0;
    return Math.max(0, toFiniteNumber((bid.meta as Record<string, unknown>).inheritSpentTrackedAmount));
};

const extendCloseDate = (options: {
    now: Date;
    closeAt: Date;
    turnMinutes: number;
    availableLatestBidCloseDate?: Date | null;
}): Date => {
    const { now, closeAt, turnMinutes, availableLatestBidCloseDate } = options;
    const extendMinutes = Math.max(MIN_EXTENSION_MINUTES_PER_BID, turnMinutes * COEFF_EXTENSION_MINUTES_PER_BID);
    const extended = new Date(now.getTime() + extendMinutes * 60 * 1000);
    if (extended.getTime() <= closeAt.getTime()) {
        return closeAt;
    }
    if (availableLatestBidCloseDate && extended.getTime() > availableLatestBidCloseDate.getTime()) {
        return availableLatestBidCloseDate;
    }
    return extended;
};

const shouldUsePrevBid = (highestBid: AuctionBidRow | null, myPrevBid: AuctionBidRow | null): AuctionBidRow | null => {
    if (!myPrevBid) {
        return null;
    }
    if (!highestBid) {
        return myPrevBid;
    }
    if (highestBid.id !== myPrevBid.id) {
        return null;
    }
    return myPrevBid;
};

type QueryClient = Pick<GamePrismaClient, '$queryRaw'>;

const loadAuction = async (prisma: QueryClient, auctionId: number): Promise<AuctionRow | null> => {
    const rows = await prisma.$queryRaw<AuctionRow[]>(
        GamePrisma.sql`
            SELECT id,
                type,
                target_code as "targetCode",
                host_general_id as "hostGeneralId",
                detail,
                status,
                close_at as "closeAt",
                close_tick as "closeTick",
                latest_event_id as "latestEventId"
            FROM auction
            WHERE id = ${auctionId}
            FOR UPDATE
        `
    );
    return rows[0] ?? null;
};

const loadHighestBid = async (
    prisma: QueryClient,
    auctionId: number,
    isReverse: boolean
): Promise<AuctionBidRow | null> => {
    const rows = await prisma.$queryRaw<AuctionBidRow[]>(
        isReverse
            ? GamePrisma.sql`
                SELECT id, general_id as "generalId", amount, meta
                FROM auction_bid
                WHERE auction_id = ${auctionId}
                ORDER BY amount ASC, id ASC
                LIMIT 1
              `
            : GamePrisma.sql`
                SELECT id, general_id as "generalId", amount, meta
                FROM auction_bid
                WHERE auction_id = ${auctionId}
                ORDER BY amount DESC, id ASC
                LIMIT 1
              `
    );
    return rows[0] ?? null;
};

const loadMyPrevBid = async (
    prisma: QueryClient,
    auctionId: number,
    generalId: number,
    isReverse: boolean
): Promise<AuctionBidRow | null> => {
    const rows = await prisma.$queryRaw<AuctionBidRow[]>(
        isReverse
            ? GamePrisma.sql`
                SELECT id, general_id as "generalId", amount, meta
                FROM auction_bid
                WHERE auction_id = ${auctionId} AND general_id = ${generalId}
                ORDER BY amount ASC, id ASC
                LIMIT 1
              `
            : GamePrisma.sql`
                SELECT id, general_id as "generalId", amount, meta
                FROM auction_bid
                WHERE auction_id = ${auctionId} AND general_id = ${generalId}
                ORDER BY amount DESC, id ASC
                LIMIT 1
              `
    );
    return rows[0] ?? null;
};

const resolveUserId = async (prisma: QueryClient, generalId: number): Promise<string | null> => {
    const rows = await prisma.$queryRaw<{ userId: string | null }[]>(
        GamePrisma.sql`SELECT user_id as "userId" FROM general WHERE id = ${generalId}`
    );
    return rows[0]?.userId ?? null;
};

export const createAuctionBidder = async (options: {
    databaseUrl: string;
    world: InMemoryTurnWorld;
}): Promise<AuctionBidder> => {
    const connector = createGamePostgresConnector({ url: options.databaseUrl });
    await connector.connect();
    const prisma = connector.prisma;
    const world = options.world;
    const itemLoader = new ItemLoader();

    return {
        bid: async (command, commandDb): Promise<TurnDaemonCommandResult> => {
            const db = commandDb ?? prisma;
            const auction = await loadAuction(db, command.auctionId);
            if (!auction) {
                return { type: 'auctionBid', ok: false, auctionId: command.auctionId, reason: '경매가 없습니다.' };
            }
            if (auction.status !== 'OPEN') {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '경매가 종료되었습니다.',
                };
            }
            const processingNow = world.getGameNow(new Date());
            const { bidAt, bidTick } = resolveAuctionBidTiming(world, processingNow, command.acceptedGameTick);
            if (hasAuctionClosePassed(auction, bidAt, bidTick)) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '경매가 종료되었습니다.',
                };
            }

            const detail = parseDetail(auction.detail);
            const isReverse = detail.isReverse === true;
            const highestBid = await loadHighestBid(db, command.auctionId, isReverse);
            const myPrevBidRaw = await loadMyPrevBid(db, command.auctionId, command.generalId, isReverse);
            const myPrevBid = shouldUsePrevBid(highestBid, myPrevBidRaw);

            if (highestBid) {
                if (!isReverse && command.amount <= highestBid.amount) {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '현재입찰가보다 높게 입찰해야 합니다.',
                    };
                }
                if (isReverse && command.amount >= highestBid.amount) {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '현재입찰가보다 낮게 입찰해야 합니다.',
                    };
                }
            } else if (detail.startBidAmount && command.amount < detail.startBidAmount) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '시작가보다 낮습니다.',
                };
            }
            if (!isReverse && detail.finishBidAmount != null && command.amount > detail.finishBidAmount) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '즉시판매가보다 높을 수 없습니다.',
                };
            }
            if (isReverse && detail.finishBidAmount != null && command.amount < detail.finishBidAmount) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '즉시판매가보다 낮을 수 없습니다.',
                };
            }

            if (auction.type === 'UNIQUE_ITEM' && highestBid) {
                if (command.amount < highestBid.amount * 1.01) {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '현재입찰가보다 1% 높게 입찰해야 합니다.',
                    };
                }
                if (command.amount < highestBid.amount + 10) {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '현재입찰가보다 10 포인트 높게 입찰해야 합니다.',
                    };
                }
            }

            const morePoint = command.amount - (myPrevBid?.amount ?? 0);
            if (morePoint <= 0) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '입찰가가 유효하지 않습니다.',
                };
            }

            const general = world.getGeneralById(command.generalId);
            if (!general) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '장수 정보를 찾을 수 없습니다.',
                };
            }
            if (auction.type === 'UNIQUE_ITEM') {
                const itemKey = auction.targetCode;
                if (!itemKey || !isItemKey(itemKey)) {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '아이템이 올바르지 않습니다.',
                    };
                }
                const item = await itemLoader.load(itemKey).catch(() => null);
                if (!item || item.buyable) {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: item ? '구매할 수 있는 아이템입니다.' : '아이템 정보를 불러올 수 없습니다.',
                    };
                }

                const currentSlotItem = general.role.items[item.slot];
                if (currentSlotItem && currentSlotItem !== 'None' && isItemKey(currentSlotItem)) {
                    const currentItem = await itemLoader.load(currentSlotItem).catch(() => null);
                    if (currentItem && !currentItem.buyable) {
                        return {
                            type: 'auctionBid',
                            ok: false,
                            auctionId: command.auctionId,
                            reason:
                                currentSlotItem === itemKey
                                    ? '이미 그 유니크를 가지고 있습니다.'
                                    : '이미 다른 유니크를 가지고 있습니다.',
                        };
                    }
                }

                const otherHighestBids = await db.$queryRaw<Array<{ auctionId: number; targetCode: string | null }>>(
                    GamePrisma.sql`
                        SELECT candidate.id as "auctionId", candidate.target_code as "targetCode"
                        FROM auction candidate
                        INNER JOIN LATERAL (
                            SELECT bid.general_id
                            FROM auction_bid bid
                            WHERE bid.auction_id = candidate.id
                            ORDER BY bid.amount DESC, bid.id ASC
                            LIMIT 1
                        ) highest ON true
                        WHERE candidate.type = 'UNIQUE_ITEM'
                          AND candidate.status IN ('OPEN', 'FINALIZING')
                          AND candidate.id <> ${auction.id}
                          AND highest.general_id = ${command.generalId}
                    `
                );
                for (const other of otherHighestBids) {
                    if (!other.targetCode || !isItemKey(other.targetCode)) {
                        continue;
                    }
                    const otherItem = await itemLoader.load(other.targetCode).catch(() => null);
                    if (otherItem?.slot === item.slot) {
                        return {
                            type: 'auctionBid',
                            ok: false,
                            auctionId: command.auctionId,
                            reason: '1순위 입찰자인 경매중에 같은 부위가 있습니다.',
                        };
                    }
                }
            }
            if (auction.type !== 'UNIQUE_ITEM' && auction.hostGeneralId === general.id) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '자신이 연 경매에 입찰할 수 없습니다.',
                };
            }

            if (auction.type === 'BUY_RICE' && !hasEnoughResourceForAuctionBid(general.gold, morePoint)) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '금이 부족합니다.',
                };
            }
            if (auction.type === 'SELL_RICE' && !hasEnoughResourceForAuctionBid(general.rice, morePoint)) {
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '쌀이 부족합니다.',
                };
            }

            if (
                auction.type !== 'UNIQUE_ITEM' &&
                highestBid &&
                highestBid.generalId !== command.generalId &&
                !myPrevBid
            ) {
                const prev = world.getGeneralById(highestBid.generalId);
                if (!prev) {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '환불 대상을 찾을 수 없습니다.',
                    };
                }
            }

            const turnMinutes = Math.max(1, Math.round(world.getState().tickSeconds / 60));
            const availableLatestBidCloseDate = detail.availableLatestBidCloseDate
                ? new Date(detail.availableLatestBidCloseDate)
                : null;
            let nextCloseAt = extendCloseDate({
                now: bidAt,
                closeAt: auction.closeAt,
                turnMinutes,
                availableLatestBidCloseDate,
            });
            if (
                auction.type !== 'UNIQUE_ITEM' &&
                detail.finishBidAmount != null &&
                command.amount === detail.finishBidAmount
            ) {
                nextCloseAt = new Date(bidAt.getTime() + turnMinutes * 60_000);
            }

            const eventId = randomUUID();
            const eventAt = bidAt;
            const rankTrackedAmount = auction.type === 'UNIQUE_ITEM' ? readRankTrackedAmount(myPrevBid) + morePoint : 0;
            const previousRankTrackedAmount = readRankTrackedAmount(highestBid);

            try {
                const persistBid = async (tx: GamePrisma.TransactionClient): Promise<void> => {
                    await tx.$executeRaw(
                        GamePrisma.sql`
                            INSERT INTO auction_bid (auction_id, general_id, amount, event_id, event_at, meta)
                            VALUES (
                                ${command.auctionId},
                                ${command.generalId},
                                ${command.amount},
                                ${eventId},
                                ${eventAt},
                                ${JSON.stringify({
                                    tryExtendCloseDate: command.tryExtendCloseDate ?? true,
                                    ...(auction.type === 'UNIQUE_ITEM'
                                        ? { inheritSpentTrackedAmount: rankTrackedAmount }
                                        : {}),
                                })}::jsonb
                            )
                        `
                    );

                    // 같은 논리 tick의 연속 입찰은 시각이 같으므로, UUID 정렬이 아니라
                    // 읽어 둔 이벤트 ID를 버전 토큰으로 사용해 경합만 거절한다.
                    const updated = await tx.$executeRaw(
                        GamePrisma.sql`
                            UPDATE auction
                            SET close_at = ${nextCloseAt},
                                close_tick = ${BigInt(world.dateToGameTick(nextCloseAt))},
                                latest_event_id = ${eventId},
                                latest_event_at = ${eventAt},
                                updated_at = ${eventAt}
                            WHERE id = ${command.auctionId}
                              AND status = 'OPEN'
                              AND latest_event_id = ${auction.latestEventId}
                        `
                    );

                    if (updated === 0) {
                        throw new Error('CONFLICT');
                    }

                    if (auction.type === 'UNIQUE_ITEM') {
                        const userId = await resolveUserId(tx, command.generalId);
                        if (!userId) {
                            throw new Error('USER_NOT_FOUND');
                        }
                        const deductedRows = await tx.$queryRaw<Array<{ value: number }>>(
                            GamePrisma.sql`
                                UPDATE inheritance_point
                                SET value = value - ${morePoint},
                                    updated_at = ${eventAt}
                                WHERE user_id = ${userId}
                                  AND key = 'previous'
                                  AND value >= ${morePoint}
                                RETURNING value
                            `
                        );
                        if (deductedRows.length === 0) {
                            throw new Error('INSUFFICIENT_POINT');
                        }
                        await tx.$executeRaw(
                            GamePrisma.sql`
                                INSERT INTO rank_data (nation_id, general_id, type, value)
                                SELECT nation_id, id, 'inherit_spent_dyn', ${morePoint}
                                FROM general
                                WHERE id = ${command.generalId}
                                ON CONFLICT (general_id, type)
                                DO UPDATE SET
                                    nation_id = EXCLUDED.nation_id,
                                    value = rank_data.value + EXCLUDED.value
                            `
                        );

                        if (highestBid && highestBid.generalId !== command.generalId && !myPrevBid) {
                            const prevUserId = await resolveUserId(tx, highestBid.generalId);
                            if (!prevUserId) {
                                throw new Error('USER_NOT_FOUND');
                            }
                            await tx.$executeRaw(
                                GamePrisma.sql`
                                    INSERT INTO inheritance_point (user_id, key, value, updated_at)
                                    VALUES (${prevUserId}, 'previous', ${highestBid.amount}, ${eventAt})
                                    ON CONFLICT (user_id, key)
                                    DO UPDATE SET
                                        value = inheritance_point.value + EXCLUDED.value,
                                        updated_at = EXCLUDED.updated_at
                                `
                            );
                            await tx.$executeRaw(
                                GamePrisma.sql`
                                    UPDATE rank_data
                                    SET value = GREATEST(0, value - ${previousRankTrackedAmount})
                                    WHERE general_id = ${highestBid.generalId}
                                      AND type = 'inherit_spent_dyn'
                                `
                            );
                        }
                    }
                };
                if (commandDb) {
                    await commandDb.$executeRawUnsafe('SAVEPOINT auction_bid_attempt');
                    try {
                        await persistBid(commandDb);
                        await commandDb.$executeRawUnsafe('RELEASE SAVEPOINT auction_bid_attempt');
                    } catch (error) {
                        await commandDb.$executeRawUnsafe('ROLLBACK TO SAVEPOINT auction_bid_attempt');
                        await commandDb.$executeRawUnsafe('RELEASE SAVEPOINT auction_bid_attempt');
                        throw error;
                    }
                } else {
                    await prisma.$transaction(persistBid);
                }
            } catch (error) {
                const reason = error instanceof Error ? error.message : 'CONFLICT';
                if (reason === 'INSUFFICIENT_POINT') {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '유산포인트가 부족합니다.',
                    };
                }
                if (reason === 'USER_NOT_FOUND') {
                    return {
                        type: 'auctionBid',
                        ok: false,
                        auctionId: command.auctionId,
                        reason: '장수 정보를 찾을 수 없습니다.',
                    };
                }
                return {
                    type: 'auctionBid',
                    ok: false,
                    auctionId: command.auctionId,
                    reason: '경매가 취소되었습니다.',
                };
            }

            if (auction.type === 'UNIQUE_ITEM') {
                world.updateGeneral(command.generalId, {
                    inheritancePoints: {
                        ...general.inheritancePoints,
                        previous: toFiniteNumber(general.inheritancePoints?.previous) - morePoint,
                    },
                    meta: {
                        ...general.meta,
                        inherit_spent_dyn: toFiniteNumber(general.meta.inherit_spent_dyn) + morePoint,
                    },
                });
                if (highestBid && highestBid.generalId !== command.generalId && !myPrevBid) {
                    const prev = world.getGeneralById(highestBid.generalId);
                    if (prev) {
                        world.updateGeneral(prev.id, {
                            inheritancePoints: {
                                ...prev.inheritancePoints,
                                previous: toFiniteNumber(prev.inheritancePoints?.previous) + highestBid.amount,
                            },
                            meta: {
                                ...prev.meta,
                                inherit_spent_dyn: Math.max(
                                    0,
                                    toFiniteNumber(prev.meta.inherit_spent_dyn) - previousRankTrackedAmount
                                ),
                            },
                        });
                    }
                }
            } else {
                const resourceType = auction.type === 'BUY_RICE' ? 'gold' : 'rice';
                world.updateGeneral(command.generalId, {
                    gold: resourceType === 'gold' ? general.gold - morePoint : general.gold,
                    rice: resourceType === 'rice' ? general.rice - morePoint : general.rice,
                });

                if (highestBid && highestBid.generalId !== command.generalId && !myPrevBid) {
                    const prev = world.getGeneralById(highestBid.generalId);
                    if (prev) {
                        world.updateGeneral(highestBid.generalId, {
                            gold: resourceType === 'gold' ? prev.gold + highestBid.amount : prev.gold,
                            rice: resourceType === 'rice' ? prev.rice + highestBid.amount : prev.rice,
                        });
                    }
                }
            }

            if (highestBid && highestBid.generalId !== command.generalId && !myPrevBid) {
                const previousBidder = world.getGeneralById(highestBid.generalId);
                if (previousBidder) {
                    world.queueMessage(
                        buildAuctionOutbidRefundMessage({
                            auctionId: auction.id,
                            title: detail.title,
                            bidder: previousBidder,
                            nation: world.getNationById(previousBidder.nationId),
                            time: eventAt,
                        })
                    );
                }
            }

            return {
                type: 'auctionBid',
                ok: true,
                auctionId: command.auctionId,
                closeAt: nextCloseAt.toISOString(),
            };
        },
        close: async (): Promise<void> => {
            await connector.disconnect();
        },
    };
};
