import { protectedProcedure, publicProcedure, router } from "../_core/trpc.ts";
import { z } from "zod";
import { getBingoRooms, getUserBingoCards, debitWallet, creditWallet, writeAuditLog, getDb } from "../db.ts";
import { eq, and, sql } from "drizzle-orm";
import { bingoCards, bingoRooms, bingoSessions } from "../../drizzle/schema.ts";

function generateBingoCard(): number[][] {
  // 5x5 bingo card: B(1-15), I(16-30), N(31-45), G(46-60), O(61-75)
  const ranges = [[1, 15], [16, 30], [31, 45], [46, 60], [61, 75]];
  const card: number[][] = [];
  for (let col = 0; col < 5; col++) {
    const [min, max] = ranges[col];
    const nums = new Set<number>();
    while (nums.size < 5) nums.add(Math.floor(Math.random() * (max - min + 1)) + min);
    const colNums = Array.from(nums);
    // Free space in center
    if (col === 2) colNums[2] = 0;
    card.push(colNums);
  }
  return card;
}

function checkBingo(card: number[][], daubed: boolean[][], calledBalls: number[]): boolean {
  // Check rows
  for (let row = 0; row < 5; row++) {
    if (card.every((col, c) => card[c][row] === 0 || calledBalls.includes(card[c][row]))) return true;
  }
  // Check columns
  for (let col = 0; col < 5; col++) {
    if (card[col].every((num, r) => num === 0 || calledBalls.includes(num))) return true;
  }
  // Check diagonals
  const diag1 = [0, 1, 2, 3, 4].every(i => card[i][i] === 0 || calledBalls.includes(card[i][i]));
  const diag2 = [0, 1, 2, 3, 4].every(i => card[i][4 - i] === 0 || calledBalls.includes(card[i][4 - i]));
  return diag1 || diag2;
}

export const bingoRouter = router({
  getRooms: publicProcedure.query(async () => {
    return getBingoRooms();
  }),

  getOrCreateSession: protectedProcedure
    .input(z.object({ roomId: z.number() }))
    .mutation(async ({ input }) => {
      const db = await getDb();
      if (!db) throw new Error("DB unavailable");
      const rooms = await db.select().from(bingoRooms).where(eq(bingoRooms.id, input.roomId)).limit(1);
      const room = rooms[0];
      if (!room) throw new Error("Room not found");

      if (room.currentSession) {
        const sessions = await db.select().from(bingoSessions).where(and(eq(bingoSessions.id, room.currentSession), eq(bingoSessions.status, "active"))).limit(1);
        if (sessions[0]) return { sessionId: sessions[0].id, calledBalls: sessions[0].calledBalls as number[] };
      }

      // Create new session
      await db.insert(bingoSessions).values({ roomId: input.roomId, calledBalls: JSON.stringify([]), ballCount: 0, prizePool: "0.00", status: "active" });
      const newSession = await db.select().from(bingoSessions).where(eq(bingoSessions.roomId, input.roomId)).orderBy(sql`id DESC`).limit(1);
      await db.update(bingoRooms).set({ currentSession: newSession[0].id }).where(eq(bingoRooms.id, input.roomId));
      return { sessionId: newSession[0].id, calledBalls: [] };
    }),

  purchaseCards: protectedProcedure
    .input(z.object({ roomId: z.number(), sessionId: z.number(), quantity: z.number().min(1).max(4) }))
    .mutation(async ({ ctx, input }) => {
      const db = await getDb();
      if (!db) throw new Error("DB unavailable");
      const rooms = await db.select().from(bingoRooms).where(eq(bingoRooms.id, input.roomId)).limit(1);
      const room = rooms[0];
      if (!room) throw new Error("Room not found");

      const totalCost = parseFloat(room.ticketPrice) * input.quantity;
      await debitWallet(ctx.user.id, room.currency, totalCost, "bingo_purchase", `Bingo cards x${input.quantity} - ${room.name}`, String(input.sessionId));

      // Update prize pool
      await db.update(bingoSessions).set({ prizePool: sql`${bingoSessions.prizePool} + ${totalCost}` }).where(eq(bingoSessions.id, input.sessionId));

      const cards = [];
      for (let i = 0; i < input.quantity; i++) {
        const numbers = generateBingoCard();
        const daubed = Array.from({ length: 5 }, (_, c) => Array.from({ length: 5 }, (_, r) => numbers[c][r] === 0));
        await db.insert(bingoCards).values({
          sessionId: input.sessionId,
          userId: ctx.user.id,
          roomId: input.roomId,
          numbers: JSON.stringify(numbers),
          daubed: JSON.stringify(daubed),
          hasBingo: false,
        });
        cards.push({ numbers, daubed });
      }
      return { cards, totalCost, currency: room.currency };
    }),

  getMyCards: protectedProcedure
    .input(z.object({ sessionId: z.number() }))
    .query(async ({ ctx, input }) => {
      return getUserBingoCards(ctx.user.id, input.sessionId);
    }),

  callBall: protectedProcedure
    .input(z.object({ sessionId: z.number() }))
    .mutation(async ({ ctx, input }) => {
      if (ctx.user.role !== "admin" && ctx.user.role !== "staff") throw new Error("Unauthorized");
      const db = await getDb();
      if (!db) throw new Error("DB unavailable");
      const sessions = await db.select().from(bingoSessions).where(eq(bingoSessions.id, input.sessionId)).limit(1);
      const session = sessions[0];
      if (!session || session.status !== "active") throw new Error("Session not active");

      const calledBalls = session.calledBalls as number[];
      const remaining = Array.from({ length: 75 }, (_, i) => i + 1).filter(n => !calledBalls.includes(n));
      if (remaining.length === 0) throw new Error("All balls called");

      const newBall = remaining[Math.floor(Math.random() * remaining.length)];
      const newCalledBalls = [...calledBalls, newBall];
      await db.update(bingoSessions).set({ calledBalls: JSON.stringify(newCalledBalls), ballCount: newCalledBalls.length }).where(eq(bingoSessions.id, input.sessionId));

      // Check for winners
      const cards = await db.select().from(bingoCards).where(and(eq(bingoCards.sessionId, input.sessionId), eq(bingoCards.hasBingo, false)));
      const winners = [];
      for (const card of cards) {
        const numbers = card.numbers as number[][];
        const daubed = card.daubed as boolean[][];
        if (checkBingo(numbers, daubed, newCalledBalls)) {
          await db.update(bingoCards).set({ hasBingo: true }).where(eq(bingoCards.id, card.id));
          winners.push(card.userId);
        }
      }

      return { ball: newBall, calledBalls: newCalledBalls, winners };
    }),

  checkWin: protectedProcedure
    .input(z.object({ cardId: z.number(), sessionId: z.number() }))
    .mutation(async ({ ctx, input }) => {
      const db = await getDb();
      if (!db) throw new Error("DB unavailable");
      const cards = await db.select().from(bingoCards).where(and(eq(bingoCards.id, input.cardId), eq(bingoCards.userId, ctx.user.id))).limit(1);
      const card = cards[0];
      if (!card) throw new Error("Card not found");
      if (card.hasBingo) return { hasBingo: true, alreadyClaimed: true };

      const sessions = await db.select().from(bingoSessions).where(eq(bingoSessions.id, input.sessionId)).limit(1);
      const session = sessions[0];
      if (!session) throw new Error("Session not found");

      const calledBalls = session.calledBalls as number[];
      const numbers = card.numbers as number[][];
      const daubed = card.daubed as boolean[][];
      const hasBingo = checkBingo(numbers, daubed, calledBalls);

      if (hasBingo) {
        const prizePool = parseFloat(session.prizePool);
        const winAmount = prizePool * 0.8; // 80% of prize pool to winner
        await db.update(bingoCards).set({ hasBingo: true, winAmount: winAmount.toFixed(2) }).where(eq(bingoCards.id, card.id));
        const rooms = await db.select().from(bingoRooms).where(eq(bingoRooms.id, card.roomId)).limit(1);
        if (rooms[0]) {
          await creditWallet(ctx.user.id, rooms[0].currency, winAmount, "bingo_win", `Bingo win! Session #${input.sessionId}`, String(input.sessionId));
        }
        return { hasBingo: true, winAmount };
      }
      return { hasBingo: false };
    }),
});
