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

export const economyService = {
    /** Transfer currency between two users; validates sufficient balance and creates transaction records for both sides. */
    transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => {
        if (amount <= 0n) {
            throw new UserError("Amount must be positive");
        }

        if (fromUserId === toUserId) {
            throw new UserError("Cannot transfer to self");
        }

        return await withTransaction(async (txFn) => {
            // Check sender balance
            const sender = await txFn.query.users.findFirst({
                where: eq(users.id, BigInt(fromUserId)),
            });

            if (!sender) {
                throw new UserError("Sender not found");
            }

            if ((sender.balance ?? 0n) < amount) {
                throw new UserError("Insufficient funds");
            }

            // Deduct from sender
            await txFn.update(users)
                .set({
                    balance: sql`${users.balance} - ${amount}`,
                })
                .where(eq(users.id, BigInt(fromUserId)));

            // Add to receiver
            await txFn.update(users)
                .set({
                    balance: sql`${users.balance} + ${amount}`,
                })
                .where(eq(users.id, BigInt(toUserId)));

            // Create transaction records
            // 1. Debit for sender
            await txFn.insert(transactions).values({
                userId: BigInt(fromUserId),
                amount: -amount,
                type: TransactionType.TRANSFER_OUT,
                description: `Transfer to ${toUserId}`,
            });

            // 2. Credit for receiver
            await txFn.insert(transactions).values({
                userId: BigInt(toUserId),
                amount: amount,
                type: TransactionType.TRANSFER_IN,
                description: `Transfer from ${fromUserId}`,
            });

            // Record dashboard event
            await systemEvents.emitAsync(EVENTS.DOMAIN.TRANSFER_COMPLETED, { userId: fromUserId, username: sender.username, amount, toUserId, tx: txFn });

            return { success: true, amount };
        }, tx);
    },

    /** Claim the daily reward, applying streak bonuses and weekly bonuses; enforces a UTC-midnight cooldown. */
    claimDaily: async (userId: string, tx?: Transaction) => {
        return await withTransaction(async (txFn) => {
            const now = new Date();

            // Check cooldown
            const cooldown = await txFn.query.userTimers.findFirst({
                where: and(
                    eq(userTimers.userId, BigInt(userId)),
                    eq(userTimers.type, TimerType.COOLDOWN),
                    eq(userTimers.key, TimerKey.DAILY)
                ),
            });

            if (cooldown && cooldown.expiresAt > now) {
                throw new UserError(`You have already claimed your daily reward today.\nNext claim available: <t:${Math.floor(cooldown.expiresAt.getTime() / 1000)}:F> (<t:${Math.floor(cooldown.expiresAt.getTime() / 1000)}:R>)`);
            }

            // Get user for streak logic
            const user = await txFn.query.users.findFirst({
                where: eq(users.id, BigInt(userId)),
            });

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

            let streak = (user.dailyStreak || 0) + 1;

            // Check if streak should be reset due to missing a day
            if (cooldown) {
                const timeSinceReady = now.getTime() - cooldown.expiresAt.getTime();
                // If more than 24h passed since it became ready, they missed a full calendar day
                if (timeSinceReady > 24 * 60 * 60 * 1000) {
                    streak = 1;
                }
            } else if ((user.dailyStreak || 0) > 0) {
                // If no cooldown record exists but user has a streak,
                // we'll allow one "free" increment to restore the timer state.
                // This prevents unfair resets if timers were cleared/lost.
                streak = (user.dailyStreak || 0) + 1;
            } else {
                streak = 1;
            }

            const bonus = (BigInt(streak) - 1n) * config.economy.daily.streakBonus;

            // Weekly bonus check
            const isWeeklyCurrent = streak > 0 && streak % 7 === 0;
            const weeklyBonusAmount = isWeeklyCurrent ? config.economy.daily.weeklyBonus : 0n;

            const MAX_DAILY_REWARD = 500n;
            const totalReward = (() => {
                const raw = config.economy.daily.amount + bonus + weeklyBonusAmount;
                return raw > MAX_DAILY_REWARD ? MAX_DAILY_REWARD : raw;
            })();
            await txFn.update(users)
                .set({
                    balance: sql`${users.balance} + ${totalReward}`,
                    dailyStreak: streak,
                    xp: sql`${users.xp} + 10`, // Small XP reward for daily
                })
                .where(eq(users.id, BigInt(userId)));

            // Set new cooldown (Next UTC Midnight)
            const nextReadyAt = new Date(now);
            nextReadyAt.setUTCDate(nextReadyAt.getUTCDate() + 1);
            nextReadyAt.setUTCHours(0, 0, 0, 0);

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

            // Log Transaction
            await txFn.insert(transactions).values({
                userId: BigInt(userId),
                amount: totalReward,
                type: TransactionType.DAILY_REWARD,
                description: `Daily reward (Streak: ${streak})`,
            });

            // Record dashboard event
            await systemEvents.emitAsync(EVENTS.DOMAIN.DAILY_CLAIMED, { userId, username: user.username, amount: totalReward, tx: txFn });

            return { claimed: true, amount: totalReward, streak, nextReadyAt, isWeekly: isWeeklyCurrent, weeklyBonus: weeklyBonusAmount };
        }, tx);
    },

    /** Adjust a user's balance by the given amount and log a transaction; throws if deduction exceeds current balance. */
    modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, relatedUserId?: string | null, tx?: Transaction) => {
        return await withTransaction(async (txFn) => {
            if (amount < 0n) {
                // Check sufficient funds if removing
                const user = await txFn.query.users.findFirst({
                    where: eq(users.id, BigInt(id))
                });
                if (!user || (user.balance ?? 0n) < -amount) {
                    throw new UserError("Insufficient funds");
                }
            }

            const [user] = await txFn.update(users)
                .set({
                    balance: sql`${users.balance} + ${amount}`,
                })
                .where(eq(users.id, BigInt(id)))
                .returning();

            await txFn.insert(transactions).values({
                userId: BigInt(id),
                relatedUserId: relatedUserId ? BigInt(relatedUserId) : null,
                amount: amount,
                type: type,
                description: description,
            });

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

            return user;
        }, tx);
    },
};
