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

const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM;
const EXAM_TIMER_KEY = 'default';

export interface ExamMetadata {
    examDay: number;
    lastXp: string;
}

export enum ExamStatus {
    NOT_REGISTERED = 'NOT_REGISTERED',
    COOLDOWN = 'COOLDOWN',
    MISSED = 'MISSED',
    AVAILABLE = 'AVAILABLE',
}

export interface ExamActionResult {
    status: ExamStatus;
    nextExamAt?: Date;
    reward?: bigint;
    xpDiff?: bigint;
    multiplier?: number;
    examDay?: number;
}

export const examService = {
    /**
     * Get the current exam status for a user.
     */
    async getExamStatus(userId: string, tx?: Transaction) {
        return await withTransaction(async (txFn) => {
            const timer = await txFn.query.userTimers.findFirst({
                where: and(
                    eq(userTimers.userId, BigInt(userId)),
                    eq(userTimers.type, EXAM_TIMER_TYPE),
                    eq(userTimers.key, EXAM_TIMER_KEY)
                )
            });

            if (!timer) {
                return { status: ExamStatus.NOT_REGISTERED };
            }

            const now = new Date();
            const expiresAt = new Date(timer.expiresAt);
            expiresAt.setHours(0, 0, 0, 0);

            if (now < expiresAt) {
                return { 
                    status: ExamStatus.COOLDOWN, 
                    nextExamAt: expiresAt 
                };
            }

            const metadata = timer.metadata as unknown as ExamMetadata;
            const currentDay = now.getDay();

            if (currentDay !== metadata.examDay) {
                return { 
                    status: ExamStatus.MISSED, 
                    nextExamAt: expiresAt,
                    examDay: metadata.examDay
                };
            }

            return { 
                status: ExamStatus.AVAILABLE,
                examDay: metadata.examDay,
                lastXp: BigInt(metadata.lastXp || "0")
            };
        }, tx);
    },

    /**
     * Register a user for the first time.
     */
    async registerForExam(userId: string, username: string, tx?: Transaction): Promise<ExamActionResult> {
        return await withTransaction(async (txFn) => {
            // Ensure user exists
            const user = await userService.getOrCreateUser(userId, username, txFn);
            if (!user) throw new SystemError("Failed to get or create user.");

            const now = new Date();
            const currentDay = now.getDay();

            // Set next exam to next week
            const nextExamDate = new Date(now);
            nextExamDate.setDate(now.getDate() + 7);
            nextExamDate.setHours(0, 0, 0, 0);

            const metadata: ExamMetadata = {
                examDay: currentDay,
                lastXp: (user.xp ?? 0n).toString()
            };

            await txFn.insert(userTimers).values({
                userId: BigInt(userId),
                type: EXAM_TIMER_TYPE,
                key: EXAM_TIMER_KEY,
                expiresAt: nextExamDate,
                metadata: metadata
            });

            return {
                status: ExamStatus.NOT_REGISTERED,
                nextExamAt: nextExamDate,
                examDay: currentDay
            };
        }, tx);
    },

    /**
     * Take the exam. Handles missed exams and reward calculations.
     */
    async takeExam(userId: string, tx?: Transaction): Promise<ExamActionResult> {
        return await withTransaction(async (txFn) => {
            const user = await txFn.query.users.findFirst({
                where: eq(users.id, BigInt(userId))
            });

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

            const timer = await txFn.query.userTimers.findFirst({
                where: and(
                    eq(userTimers.userId, BigInt(userId)),
                    eq(userTimers.type, EXAM_TIMER_TYPE),
                    eq(userTimers.key, EXAM_TIMER_KEY)
                )
            });

            if (!timer) {
                return { status: ExamStatus.NOT_REGISTERED };
            }

            const now = new Date();
            const expiresAt = new Date(timer.expiresAt);
            expiresAt.setHours(0, 0, 0, 0);

            if (now < expiresAt) {
                 return { 
                    status: ExamStatus.COOLDOWN, 
                    nextExamAt: expiresAt 
                };
            }

            const metadata = timer.metadata as unknown as ExamMetadata;
            const examDay = metadata.examDay;
            const currentDay = now.getDay();

            if (currentDay !== examDay) {
                // Missed exam logic
                let daysUntil = (examDay - currentDay + 7) % 7;
                if (daysUntil === 0) daysUntil = 7;

                const nextExamDate = new Date(now);
                nextExamDate.setDate(now.getDate() + daysUntil);
                nextExamDate.setHours(0, 0, 0, 0);

                const newMetadata: ExamMetadata = {
                    examDay: examDay,
                    lastXp: (user.xp ?? 0n).toString()
                };

                await txFn.update(userTimers)
                    .set({
                        expiresAt: nextExamDate,
                        metadata: newMetadata
                    })
                    .where(and(
                        eq(userTimers.userId, BigInt(userId)),
                        eq(userTimers.type, EXAM_TIMER_TYPE),
                        eq(userTimers.key, EXAM_TIMER_KEY)
                    ));

                return {
                    status: ExamStatus.MISSED,
                    nextExamAt: nextExamDate,
                    examDay: examDay
                };
            }

            // Reward Calculation
            const lastXp = BigInt(metadata.lastXp || "0");
            const currentXp = user.xp ?? 0n;
            const diff = currentXp - lastXp;

            const multMin = config.economy.exam.multMin;
            const multMax = config.economy.exam.multMax;
            const multiplier = Math.random() * (multMax - multMin) + multMin;

            let reward = 0n;
            if (diff > 0n) {
                // Use scaled BigInt arithmetic to avoid precision loss with large XP values
                const scaledMultiplier = BigInt(Math.round(multiplier * 10000));
                reward = (diff * scaledMultiplier) / 10000n;
            }

            const nextExamDate = new Date(now);
            nextExamDate.setDate(now.getDate() + 7);
            nextExamDate.setHours(0, 0, 0, 0);

            const newMetadata: ExamMetadata = {
                examDay: examDay,
                lastXp: currentXp.toString()
            };

            // Update Timer
            await txFn.update(userTimers)
                .set({
                    expiresAt: nextExamDate,
                    metadata: newMetadata
                })
                .where(and(
                    eq(userTimers.userId, BigInt(userId)),
                    eq(userTimers.type, EXAM_TIMER_TYPE),
                    eq(userTimers.key, EXAM_TIMER_KEY)
                ));

            // Add Currency
            if (reward > 0n) {
                await txFn.update(users)
                    .set({
                        balance: sql`${users.balance} + ${reward}`
                    })
                    .where(eq(users.id, BigInt(userId)));

                // Add Transaction Record
                await txFn.insert(transactions).values({
                    userId: BigInt(userId),
                    amount: reward,
                    type: TransactionType.EXAM_REWARD,
                    description: `Weekly exam reward (XP Diff: ${diff})`,
                });
            }

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

            return {
                status: ExamStatus.AVAILABLE,
                nextExamAt: nextExamDate,
                reward,
                xpDiff: diff,
                multiplier,
                examDay
            };
        }, tx);
    }
};
