import { prisma } from '../db.js';
import { pickWeighted } from '../weighted.js';
import { applyAutoSell } from '../autoSell.js';
import { emitJackpot, emitJackpotsList } from '../realtime.js';
import { pushDrop } from '../feed.js';
import { checkAchievements } from '../achievements.js';
import { toCentsBigInt, centsJson, creditUserBalance } from '../cents.js';

const ROUND_SECONDS = Number(process.env.JACKPOT_ROUND_SECONDS || 30);
const MAX_ITEMS = Number(process.env.JACKPOT_MAX_ITEMS || 3);
const MIN_PLAYERS = 2;
const SPIN_MS = 5500;
/** Sentinel endsAt while waiting for enough players (timer not armed). */
const WAITING_ENDS_AT = new Date('2099-01-01T00:00:00.000Z');

const resolveTimers = new Map();
const spinFollowUps = new Map();

function isTimerWaiting(endsAt) {
  return new Date(endsAt).getFullYear() >= 2099;
}

function uniquePlayerCount(deposits) {
  return new Set((deposits || []).map((d) => d.userId)).size;
}

function depositInclude() {
  return {
    user: { select: { id: true, username: true, avatarUrl: true } },
    inventoryItem: { include: { item: true } },
  };
}

function roundInclude() {
  return {
    deposits: { include: depositInclude() },
    winner: { select: { id: true, username: true, avatarUrl: true } },
    creator: { select: { id: true, username: true, avatarUrl: true } },
  };
}

export function serializeJackpotRound(round) {
  if (!round) return null;

  const deposits = (round.deposits || []).map((d) => ({
    id: d.id,
    userId: d.userId,
    username: d.user?.username || '',
    avatarUrl: d.user?.avatarUrl || '',
    valueCents: d.valueCents,
    inventoryItemId: d.inventoryItemId,
    item: d.inventoryItem
      ? {
          id: d.inventoryItem.item.id,
          name: d.inventoryItem.item.name,
          imageUrl: d.inventoryItem.item.imageUrl,
          rarity: d.inventoryItem.item.rarity,
          marketValue: d.valueCents,
          valueCents: d.valueCents,
          wear: d.inventoryItem.wear,
          floatValue: d.inventoryItem.floatValue,
          paintSeed: d.inventoryItem.paintSeed,
        }
      : null,
  }));

  const byUser = new Map();
  let totalValue = 0n;
  for (const d of deposits) {
    const v = toCentsBigInt(d.valueCents);
    totalValue += v;
    const prev = byUser.get(d.userId) || {
      userId: d.userId,
      username: d.username,
      avatarUrl: d.avatarUrl,
      valueCents: 0n,
      itemCount: 0,
    };
    prev.valueCents += v;
    prev.itemCount += 1;
    byUser.set(d.userId, prev);
  }

  const players = [...byUser.values()].map((p) => ({
    ...p,
    valueCents: centsJson(p.valueCents),
    chance:
      totalValue > 0n ? Number((p.valueCents * 10000n) / totalValue) / 100 : 0,
  }));

  const waitingForPlayers =
    players.length < MIN_PLAYERS || isTimerWaiting(round.endsAt);

  return {
    id: round.id,
    status: round.status,
    creatorId: round.creatorId || null,
    creatorUsername: round.creator?.username || null,
    name: `Battle ${round.id}`,
    endsAt: waitingForPlayers ? null : round.endsAt,
    waitingForPlayers,
    roundSeconds: ROUND_SECONDS,
    minPlayers: MIN_PLAYERS,
    createdAt: round.createdAt,
    resolvedAt: round.resolvedAt || null,
    winnerUserId: round.winnerUserId || null,
    winnerUsername: round.winner?.username || null,
    totalValue: centsJson(totalValue),
    maxItemsPerPlayer: MAX_ITEMS,
    players,
    deposits,
  };
}

async function loadRound(id) {
  return prisma.jackpotRound.findUnique({
    where: { id: Number(id) },
    include: roundInclude(),
  });
}

export async function getJackpotRound(id) {
  return serializeJackpotRound(await loadRound(id));
}

export async function listJackpotBattles() {
  const rounds = await prisma.jackpotRound.findMany({
    where: {
      OR: [
        { status: 'spinning' },
        { status: 'open', deposits: { some: {} } },
      ],
    },
    include: roundInclude(),
    orderBy: { id: 'desc' },
  });
  return rounds.map(serializeJackpotRound);
}

export async function broadcastJackpotsList() {
  const battles = await listJackpotBattles();
  emitJackpotsList({ battles });
  return battles;
}

function clearResolveTimer(roundId) {
  const t = resolveTimers.get(roundId);
  if (t) {
    clearTimeout(t);
    resolveTimers.delete(roundId);
  }
}

function clearSpinFollowUp(roundId) {
  const t = spinFollowUps.get(roundId);
  if (t) {
    clearTimeout(t);
    spinFollowUps.delete(roundId);
  }
}

function clearTimers(roundId) {
  clearResolveTimer(roundId);
  clearSpinFollowUp(roundId);
}

function scheduleResolve(roundId, endsAt) {
  clearResolveTimer(roundId);
  if (isTimerWaiting(endsAt)) return;
  const delay = Math.max(0, new Date(endsAt).getTime() - Date.now());
  resolveTimers.set(
    roundId,
    setTimeout(() => {
      resolveJackpotRound(roundId).catch((err) =>
        console.error('jackpot resolve', err)
      );
    }, delay)
  );
}

/**
 * Recover timers for all active battles (boot). Does not create empty rounds.
 * Cancels orphan open battles with zero deposits (legacy singleton leftovers).
 */
export async function ensureJackpotRound() {
  await prisma.jackpotRound.updateMany({
    where: {
      status: 'open',
      deposits: { none: {} },
    },
    data: { status: 'cancelled', resolvedAt: new Date() },
  });

  const rounds = await prisma.jackpotRound.findMany({
    where: { status: { in: ['open', 'spinning'] } },
    include: { deposits: true },
  });

  for (const round of rounds) {
    if (round.status === 'spinning') {
      clearSpinFollowUp(round.id);
      spinFollowUps.set(
        round.id,
        setTimeout(() => {
          finishJackpotSpin(round.id).catch((err) => console.error(err));
        }, 1000)
      );
      continue;
    }

    const players = uniquePlayerCount(round.deposits);
    if (players < MIN_PLAYERS) {
      if (!isTimerWaiting(round.endsAt)) {
        await prisma.jackpotRound.update({
          where: { id: round.id },
          data: { endsAt: WAITING_ENDS_AT },
        });
      }
      clearResolveTimer(round.id);
      continue;
    }

    if (isTimerWaiting(round.endsAt)) {
      const endsAt = new Date(Date.now() + ROUND_SECONDS * 1000);
      await prisma.jackpotRound.update({
        where: { id: round.id },
        data: { endsAt },
      });
      scheduleResolve(round.id, endsAt);
      continue;
    }

    if (new Date(round.endsAt).getTime() <= Date.now()) {
      await resolveJackpotRound(round.id);
      continue;
    }

    scheduleResolve(round.id, round.endsAt);
  }

  return listJackpotBattles();
}

async function applyDeposits(tx, roundId, userId, ids) {
  const existingCount = await tx.jackpotDeposit.count({
    where: { roundId, userId },
  });
  if (existingCount + ids.length > MAX_ITEMS) {
    throw Object.assign(
      new Error(`You can bet at most ${MAX_ITEMS} items this round`),
      { status: 400 }
    );
  }

  const invItems = await tx.inventoryItem.findMany({
    where: { id: { in: ids }, userId, locked: false, staked: false },
    include: { item: true },
  });
  if (invItems.length !== ids.length) {
    throw Object.assign(new Error('Invalid, locked, or staked inventory items'), {
      status: 400,
    });
  }

  for (const inv of invItems) {
    await tx.inventoryItem.update({
      where: { id: inv.id },
      data: { locked: true },
    });
    await tx.jackpotDeposit.create({
      data: {
        roundId,
        userId,
        inventoryItemId: inv.id,
        valueCents: inv.valueCents,
      },
    });
    await tx.transaction.create({
      data: {
        userId,
        type: 'jackpot_bet',
        amount: '0',
        meta: JSON.stringify({
          roundId,
          inventoryItemId: inv.id,
          itemId: inv.itemId,
          valueCents: centsJson(inv.valueCents),
          wear: inv.wear,
          floatValue: inv.floatValue,
        }),
      },
    });
  }
}

/**
 * Create a new battle by placing the first bet.
 */
export async function createJackpotBattle(userId, inventoryItemIds) {
  const ids = [...new Set((inventoryItemIds || []).map(Number).filter(Boolean))];
  if (!ids.length) {
    throw Object.assign(new Error('Select at least one item'), { status: 400 });
  }
  if (ids.length > MAX_ITEMS) {
    throw Object.assign(new Error(`Max ${MAX_ITEMS} items per bet`), { status: 400 });
  }

  const round = await prisma.$transaction(async (tx) => {
    const created = await tx.jackpotRound.create({
      data: {
        status: 'open',
        endsAt: WAITING_ENDS_AT,
        creatorId: userId,
      },
    });

    await applyDeposits(tx, created.id, userId, ids);

    return tx.jackpotRound.findUnique({
      where: { id: created.id },
      include: roundInclude(),
    });
  });

  const state = serializeJackpotRound(round);
  emitJackpot(round.id, 'jackpot:state', state);
  await broadcastJackpotsList();
  return state;
}

export async function placeJackpotBet(roundId, userId, inventoryItemIds) {
  const id = Number(roundId);
  if (!id) {
    throw Object.assign(new Error('Invalid battle'), { status: 400 });
  }

  const ids = [...new Set((inventoryItemIds || []).map(Number).filter(Boolean))];
  if (!ids.length) {
    throw Object.assign(new Error('Select at least one item'), { status: 400 });
  }
  if (ids.length > MAX_ITEMS) {
    throw Object.assign(new Error(`Max ${MAX_ITEMS} items per bet`), { status: 400 });
  }

  const round = await prisma.$transaction(async (tx) => {
    const open = await tx.jackpotRound.findUnique({ where: { id } });
    if (!open || open.status !== 'open') {
      throw Object.assign(new Error('Battle is not open'), { status: 409 });
    }
    if (!isTimerWaiting(open.endsAt) && new Date(open.endsAt).getTime() <= Date.now()) {
      throw Object.assign(new Error('Round is closing'), { status: 409 });
    }

    await applyDeposits(tx, open.id, userId, ids);

    let next = await tx.jackpotRound.findUnique({
      where: { id: open.id },
      include: roundInclude(),
    });

    if (
      uniquePlayerCount(next.deposits) >= MIN_PLAYERS &&
      isTimerWaiting(next.endsAt)
    ) {
      const endsAt = new Date(Date.now() + ROUND_SECONDS * 1000);
      next = await tx.jackpotRound.update({
        where: { id: next.id },
        data: { endsAt },
        include: roundInclude(),
      });
    }

    return next;
  });

  if (
    uniquePlayerCount(round.deposits) >= MIN_PLAYERS &&
    !isTimerWaiting(round.endsAt)
  ) {
    scheduleResolve(round.id, round.endsAt);
  }

  const state = serializeJackpotRound(round);
  emitJackpot(round.id, 'jackpot:state', state);
  await broadcastJackpotsList();
  return state;
}

export async function withdrawJackpotBet(roundId, userId, inventoryItemIds) {
  const id = Number(roundId);
  if (!id) {
    throw Object.assign(new Error('Invalid battle'), { status: 400 });
  }

  const ids = [...new Set((inventoryItemIds || []).map(Number).filter(Boolean))];
  if (!ids.length) {
    throw Object.assign(new Error('Select items to withdraw'), { status: 400 });
  }

  const round = await prisma.$transaction(async (tx) => {
    const open = await tx.jackpotRound.findUnique({ where: { id } });
    if (!open || open.status !== 'open') {
      throw Object.assign(new Error('Battle is not open'), { status: 409 });
    }
    if (
      !isTimerWaiting(open.endsAt) &&
      new Date(open.endsAt).getTime() - Date.now() < 3000
    ) {
      throw Object.assign(new Error('Too late to withdraw'), { status: 409 });
    }

    const deposits = await tx.jackpotDeposit.findMany({
      where: {
        roundId: open.id,
        userId,
        inventoryItemId: { in: ids },
      },
    });
    if (deposits.length !== ids.length) {
      throw Object.assign(new Error('Deposit not found'), { status: 400 });
    }

    for (const d of deposits) {
      await tx.jackpotDeposit.delete({ where: { id: d.id } });
      await tx.inventoryItem.update({
        where: { id: d.inventoryItemId },
        data: { locked: false },
      });
    }

    let next = await tx.jackpotRound.findUnique({
      where: { id: open.id },
      include: roundInclude(),
    });

    if (uniquePlayerCount(next.deposits) < MIN_PLAYERS && !isTimerWaiting(next.endsAt)) {
      next = await tx.jackpotRound.update({
        where: { id: next.id },
        data: { endsAt: WAITING_ENDS_AT },
        include: roundInclude(),
      });
    }

    // Creator left alone with no deposits → cancel empty battle
    if (!next.deposits.length) {
      next = await tx.jackpotRound.update({
        where: { id: next.id },
        data: { status: 'cancelled', resolvedAt: new Date() },
        include: roundInclude(),
      });
    }

    return next;
  });

  if (round.status === 'cancelled') {
    clearTimers(round.id);
    const state = serializeJackpotRound(round);
    emitJackpot(round.id, 'jackpot:state', state);
    await broadcastJackpotsList();
    return state;
  }

  if (isTimerWaiting(round.endsAt) || uniquePlayerCount(round.deposits) < MIN_PLAYERS) {
    clearResolveTimer(round.id);
  }

  const state = serializeJackpotRound(round);
  emitJackpot(round.id, 'jackpot:state', state);
  await broadcastJackpotsList();
  return state;
}

async function finishJackpotSpin(roundId) {
  let feedEntry = null;
  let winnerId = null;

  const result = await prisma.$transaction(async (tx) => {
    const round = await tx.jackpotRound.findUnique({
      where: { id: roundId },
      include: roundInclude(),
    });
    if (!round || round.status !== 'spinning') return null;

    winnerId = round.winnerUserId;
    for (const d of round.deposits) {
      await tx.inventoryItem.update({
        where: { id: d.inventoryItemId },
        data: { userId: winnerId, locked: false },
      });
    }

    const acquiredIds = round.deposits
      .filter((d) => d.userId !== winnerId)
      .map((d) => d.inventoryItemId);
    let autoSellTotal = 0n;
    if (acquiredIds.length) {
      const winner = await tx.user.findUnique({ where: { id: winnerId } });
      const acquiredItems = await tx.inventoryItem.findMany({
        where: { id: { in: acquiredIds } },
      });
      const { balanceDelta } = await applyAutoSell(tx, winner, acquiredItems);
      autoSellTotal = balanceDelta;
    }

    const totalValue = round.deposits.reduce(
      (s, d) => s + toCentsBigInt(d.valueCents),
      0n
    );
    await tx.transaction.create({
      data: {
        userId: winnerId,
        type: 'jackpot_win',
        amount: centsJson(totalValue),
        meta: JSON.stringify({
          roundId: round.id,
          depositIds: round.deposits.map((d) => d.id),
          autoSellTotal: centsJson(autoSellTotal),
        }),
      },
    });

    if (autoSellTotal > 0n) {
      await creditUserBalance(tx, winnerId, autoSellTotal);
    }

    const resolved = await tx.jackpotRound.update({
      where: { id: round.id },
      data: { status: 'resolved', resolvedAt: new Date() },
      include: roundInclude(),
    });

    let best = null;
    for (const d of round.deposits) {
      const inv = d.inventoryItem;
      const item = inv?.item;
      if (!item) continue;
      if (!best || toCentsBigInt(d.valueCents) > toCentsBigInt(best.deposit.valueCents)) {
        best = { deposit: d, item, inv };
      }
    }

    const winnerUser =
      round.winner ||
      round.deposits.find((d) => d.userId === winnerId)?.user ||
      null;

    if (best && winnerUser) {
      const count = round.deposits.filter(
        (d) => d.inventoryItem?.itemId === best.item.id
      ).length;
      feedEntry = {
        source: 'jackpot',
        caseName: `Battle ${round.id}`,
        count,
        openCount: round.deposits.length,
        inventoryIds: round.deposits.map((d) => d.inventoryItemId).filter(Boolean),
        username: winnerUser.username,
        avatarUrl: winnerUser.avatarUrl || '',
        item: {
          inventoryId: best.deposit.inventoryItemId,
          id: best.item.id,
          name: best.item.name,
          imageUrl: best.item.imageUrl,
          rarity: best.item.rarity,
          marketValue: best.deposit.valueCents,
          valueCents: best.deposit.valueCents,
          wear: best.inv.wear,
          floatValue: best.inv.floatValue,
          paintSeed: best.inv.paintSeed,
        },
      };
    }

    return resolved;
  });

  clearTimers(roundId);

  if (!result) return;

  if (winnerId) {
    checkAchievements(winnerId).catch((err) => console.error('achievements', err));
  }

  if (feedEntry) {
    try {
      pushDrop(feedEntry);
    } catch (err) {
      console.error('jackpot feed drop', err);
    }
  }

  const state = serializeJackpotRound(result);
  emitJackpot(roundId, 'jackpot:result', {
    previousRoundId: roundId,
    ...state,
  });
  await broadcastJackpotsList();
}

export async function resolveJackpotRound(roundId) {
  const id = Number(roundId);
  clearTimers(id);

  const spun = await prisma.$transaction(async (tx) => {
    const open = await tx.jackpotRound.findUnique({
      where: { id },
      include: {
        deposits: { include: depositInclude() },
      },
    });
    if (!open || open.status !== 'open') return null;

    const players = uniquePlayerCount(open.deposits);
    if (players < MIN_PLAYERS || !open.deposits.length) {
      const waiting = await tx.jackpotRound.update({
        where: { id: open.id },
        data: { endsAt: WAITING_ENDS_AT },
        include: roundInclude(),
      });
      return { kind: 'waiting', updated: waiting };
    }

    const byUser = new Map();
    for (const d of open.deposits) {
      byUser.set(
        d.userId,
        (byUser.get(d.userId) || 0n) + toCentsBigInt(d.valueCents)
      );
    }
    const maxV = [...byUser.values()].reduce((a, b) => (a > b ? a : b), 0n);
    const weightEntries =
      maxV > BigInt(Number.MAX_SAFE_INTEGER)
        ? [...byUser.entries()].map(([userId, v]) => ({
            userId,
            dropRate: Number((v * 1_000_000n) / maxV) || 1,
          }))
        : [...byUser.entries()].map(([userId, v]) => ({
            userId,
            dropRate: Number(v),
          }));

    const winner = pickWeighted(weightEntries);

    const winnerDeposits = open.deposits.filter((d) => d.userId === winner.userId);
    const reelDepositId = winnerDeposits[0]?.id || null;

    const updated = await tx.jackpotRound.update({
      where: { id: open.id },
      data: { status: 'spinning', winnerUserId: winner.userId },
      include: roundInclude(),
    });

    return { kind: 'spin', updated, reelItemId: reelDepositId };
  });

  if (!spun) return;

  if (spun.kind === 'waiting') {
    const state = serializeJackpotRound(spun.updated);
    clearResolveTimer(id);
    emitJackpot(id, 'jackpot:state', state);
    await broadcastJackpotsList();
    return state;
  }

  const { updated, reelItemId } = spun;
  const state = serializeJackpotRound(updated);
  emitJackpot(id, 'jackpot:spin', {
    ...state,
    reelWinnerItemId: reelItemId,
  });
  await broadcastJackpotsList();

  spinFollowUps.set(
    updated.id,
    setTimeout(() => {
      finishJackpotSpin(updated.id).catch((err) =>
        console.error('jackpot finish', err)
      );
    }, SPIN_MS + 200)
  );

  return state;
}

/** @deprecated use listJackpotBattles / getJackpotRound */
export async function getJackpotState() {
  const battles = await listJackpotBattles();
  return battles[0] || null;
}

export async function broadcastJackpot(extra = {}) {
  const battles = await broadcastJackpotsList();
  return battles[0] ? { ...battles[0], ...extra } : null;
}
