import { users, userTimers } from "@db/schema";
import { eq, sql, and } from "drizzle-orm";
import { withTransaction } from "@/lib/db";
import { config } from "@shared/lib/config";
import { systemEvents, EVENTS } from "@shared/lib/events";
import type { Transaction } from "@shared/lib/types";
import { TimerKey, TimerType } from "@shared/lib/constants";
import { UserError } from "@shared/lib/errors";

export const levelingService = {
    /** Calculate the cumulative XP required to reach a specific level. */
    getXpToReachLevel: (level: number) => {
        let total = 0;
        for (let l = 1; l < level; l++) {
            total += Math.floor(config.leveling.base * Math.pow(l, config.leveling.exponent));
        }
        return total;
    },

    /** Derive a user's level from their total accumulated XP. */
    getLevelFromXp: (totalXp: bigint) => {
        let level = 1;
        let xp = Number(totalXp);

        while (true) {
            // XP needed to complete current level and reach next
            const xpForNext = Math.floor(config.leveling.base * Math.pow(level, config.leveling.exponent));
            if (xp < xpForNext) {
                return level;
            }
            xp -= xpForNext;
            level++;
        }
    },

    /** Get the XP needed to advance from the given level to the next. */
    getXpForNextLevel: (currentLevel: number) => {
        return Math.floor(config.leveling.base * Math.pow(currentLevel, config.leveling.exponent));
    },

    /** Add XP to a user, recalculating their level and emitting a quest event. */
    addXp: async (id: string, amount: bigint, tx?: Transaction) => {
        return await withTransaction(async (txFn) => {
            // Get current state
            const user = await txFn.query.users.findFirst({
                where: eq(users.id, BigInt(id)),
            });

            if (!user) throw new UserError("User not found");

            const currentXp = user.xp ?? 0n;
            const newXp = currentXp + amount;

            // Calculate new level based on TOTAL accumulated XP
            const newLevel = levelingService.getLevelFromXp(newXp);
            const currentLevel = user.level ?? 1;
            const levelUp = newLevel > currentLevel;

            // Update user
            const [updatedUser] = await txFn.update(users)
                .set({
                    xp: newXp,
                    level: newLevel,
                })
                .where(eq(users.id, BigInt(id)))
                .returning();

            // Trigger Quest Event
            await systemEvents.emitAsync(EVENTS.DOMAIN.XP_GAINED, { userId: id, amount: Number(amount), tx: txFn });

            return { user: updatedUser, levelUp, currentLevel: newLevel };
        }, tx);
    },

    /** Award random chat XP if the user is not on cooldown; applies active XP boost multipliers. */
    processChatXp: async (id: string, tx?: Transaction) => {
        return await withTransaction(async (txFn) => {
            // check if an xp cooldown is in place
            const cooldown = await txFn.query.userTimers.findFirst({
                where: and(
                    eq(userTimers.userId, BigInt(id)),
                    eq(userTimers.type, TimerType.COOLDOWN),
                    eq(userTimers.key, TimerKey.CHAT_XP)
                ),
            });

            const now = new Date();
            if (cooldown && cooldown.expiresAt > now) {
                return { awarded: false, reason: 'cooldown' };
            }

            // Calculate random XP
            let amount = BigInt(Math.floor(Math.random() * (config.leveling.chat.maxXp - config.leveling.chat.minXp + 1)) + config.leveling.chat.minXp);

            // Check for XP Boost
            const xpBoost = await txFn.query.userTimers.findFirst({
                where: and(
                    eq(userTimers.userId, BigInt(id)),
                    eq(userTimers.type, TimerType.EFFECT),
                    eq(userTimers.key, 'xp_boost')
                )
            });

            if (xpBoost && xpBoost.expiresAt > now) {
                const multiplier = (xpBoost.metadata as any)?.multiplier || 1;
                amount = BigInt(Math.floor(Number(amount) * multiplier));
            }

            // Add XP
            const result = await levelingService.addXp(id, amount, txFn);

            // Update/Set Cooldown
            const nextReadyAt = new Date(now.getTime() + config.leveling.chat.cooldownMs);

            await txFn.insert(userTimers)
                .values({
                    userId: BigInt(id),
                    type: TimerType.COOLDOWN,
                    key: TimerKey.CHAT_XP,
                    expiresAt: nextReadyAt,
                })
                .onConflictDoUpdate({
                    target: [userTimers.userId, userTimers.type, userTimers.key],
                    set: { expiresAt: nextReadyAt },
                });

            return { awarded: true, amount, ...result };
        }, tx);
    }
};
