import { Router } from 'express';
import { prisma } from '../db.js';
import { requireAuth, publicUser } from '../middleware.js';
import { applyCatalogLuck, pickWeighted } from '../weighted.js';
import { rollInstanceValue } from '../wear.js';
import { shouldAutoSell } from '../autoSell.js';
import {
  applyOpens,
  getOrCreateProgress,
  maxOpensForKeys,
  serializeKeyProgress,
} from '../caseKeys.js';
import {
  countCompletedChapters,
  fillCatalogSlots,
  formatCaseItemsWithLuck,
} from '../catalog.js';
import { totalLuckPercent } from '../prestige.js';
import { toCentsBigInt, adjustUserBalance, spendUserBalance, centsJson } from '../cents.js';
import { checkAchievements } from '../achievements.js';

const router = Router();

function formatCase(c, luckPercent = 0, unitPrice = null) {
  const items = (c.items || []).map((ci) => ({
    id: ci.item.id,
    name: ci.item.name,
    imageUrl: ci.item.imageUrl,
    rarity: ci.item.rarity,
    marketValue: ci.item.marketValue,
    dropRate: ci.dropRate,
  }));

  return {
    id: c.id,
    name: c.name,
    imageUrl: c.imageUrl,
    price: unitPrice != null ? unitPrice : c.price,
    basePrice: c.price,
    active: c.active,
    catalogLuckPercent: luckPercent,
    items: formatCaseItemsWithLuck(items, luckPercent),
  };
}

async function loadUserLuckAndKeys(userId) {
  if (!userId) {
    return {
      luckPercent: 0,
      openBonus: 0,
      user: null,
    };
  }
  const [catalogLuck, user] = await Promise.all([
    countCompletedChapters(prisma, userId),
    prisma.user.findUnique({
      where: { id: userId },
      select: {
        prestigeChanceBonus: true,
        prestigeWearBonus: true,
        prestigeOpenBonus: true,
        prestigeQualityBonus: true,
        prestigeAnimReduction: true,
        prestigeRefundChance: true,
        prestigeRespinChance: true,
      },
    }),
  ]);
  return {
    luckPercent: totalLuckPercent(catalogLuck, user?.prestigeChanceBonus),
    openBonus: user?.prestigeOpenBonus ?? 0,
    user,
  };
}

async function loadKeyProgressMap(userId) {
  if (!userId) return new Map();
  const rows = await prisma.caseKeyProgress.findMany({
    where: { userId },
  });
  return new Map(rows.map((r) => [r.caseId, r]));
}

function rollOneDrop(weighted, user) {
  const winner = pickWeighted(weighted);
  const item = winner.caseItem.item;
  const rolled = rollInstanceValue(
    item.marketValue,
    user.prestigeWearBonus ?? 0,
    user.prestigeQualityBonus ?? 0
  );
  return { item, rolled };
}

router.get('/', async (req, res) => {
  try {
    const userId = req.session?.userId;
    const cases = await prisma.case.findMany({
      where: { active: true },
      include: {
        items: { include: { item: true } },
      },
    });
    cases.sort((a, b) => {
      const pa = toCentsBigInt(a.price);
      const pb = toCentsBigInt(b.price);
      if (pa < pb) return -1;
      if (pa > pb) return 1;
      return a.id - b.id;
    });
    const progressMap = await loadKeyProgressMap(userId);
    const { luckPercent, openBonus } = await loadUserLuckAndKeys(userId);
    res.json({
      catalogLuckPercent: luckPercent,
      cases: cases.map((c) => {
        const formatted = formatCase(c, luckPercent, userId ? c.price : Number(c.price));
        if (!userId) return formatted;
        const progress = progressMap.get(c.id);
        return {
          ...formatted,
          keyProgress: serializeKeyProgress(progress, 0, openBonus),
        };
      }),
    });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to load cases' });
  }
});

router.get('/:id', async (req, res) => {
  try {
    const id = Number(req.params.id);
    const userId = req.session?.userId;
    const c = await prisma.case.findUnique({
      where: { id },
      include: {
        items: { include: { item: true } },
      },
    });
    if (!c || !c.active) {
      return res.status(404).json({ error: 'Case not found' });
    }
    const { luckPercent, openBonus } = await loadUserLuckAndKeys(userId);
    const unitPrice = userId ? c.price : Number(c.price);
    const formatted = formatCase(c, luckPercent, unitPrice);
    if (userId) {
      const progress = await prisma.caseKeyProgress.findUnique({
        where: { userId_caseId: { userId, caseId: id } },
      });
      formatted.keyProgress = serializeKeyProgress(progress, 0, openBonus);
    }
    res.json({ case: formatted });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to load case' });
  }
});

router.post('/:id/open', requireAuth, async (req, res) => {
  try {
    const caseId = Number(req.params.id);
    const userId = req.session.userId;
    let count = Number(req.body?.count ?? 1);
    if (!Number.isFinite(count)) count = 1;
    count = Math.max(1, Math.floor(count));

    const result = await prisma.$transaction(async (tx) => {
      const c = await tx.case.findUnique({
        where: { id: caseId },
        include: {
          items: { include: { item: true } },
        },
      });

      if (!c || !c.active) {
        const err = new Error('Case not found');
        err.status = 404;
        throw err;
      }
      if (!c.items.length) {
        const err = new Error('Case has no items');
        err.status = 400;
        throw err;
      }

      const user = await tx.user.findUnique({ where: { id: userId } });
      if (!user) {
        const err = new Error('User not found');
        err.status = 401;
        throw err;
      }

      const progressRow = await getOrCreateProgress(tx, userId, caseId);
      const openBonus = user.prestigeOpenBonus ?? 0;
      const maxOpens = maxOpensForKeys(progressRow.keys, openBonus);
      if (count > maxOpens) {
        const err = new Error(`You can open at most ${maxOpens} at once for this case`);
        err.status = 400;
        throw err;
      }

      const unitPrice = toCentsBigInt(c.price);
      const totalCost = unitPrice * BigInt(count);
      // Charge up front (atomic WHERE balance >= cost). Refunded opens credit back later.
      await spendUserBalance(tx, userId, totalCost);

      const catalogLuck = await countCompletedChapters(tx, userId);
      const luckPercent = totalLuckPercent(catalogLuck, user.prestigeChanceBonus);
      const weighted = applyCatalogLuck(
        c.items.map((ci) => ({
          caseItem: ci,
          dropRate: ci.dropRate,
        })),
        luckPercent
      );

      async function createDrops(batchCount) {
        const drops = [];
        const createdTxIds = [];
        for (let i = 0; i < batchCount; i += 1) {
          const { item, rolled } = rollOneDrop(weighted, user);
          const inventoryItem = await tx.inventoryItem.create({
            data: {
              userId,
              itemId: item.id,
              floatValue: rolled.floatValue,
              wear: rolled.wear,
              paintSeed: rolled.paintSeed,
              valueCents: centsJson(rolled.valueCents),
            },
          });

          const willAutoSell = shouldAutoSell(user, rolled.valueCents);

          const openTx = await tx.transaction.create({
            data: {
              userId,
              type: 'open_case',
              amount: centsJson(-unitPrice),
              meta: JSON.stringify({
                caseId: c.id,
                caseName: c.name,
                itemId: item.id,
                itemName: item.name,
                inventoryItemId: inventoryItem.id,
                floatValue: rolled.floatValue,
                wear: rolled.wear,
                paintSeed: rolled.paintSeed,
                valueCents: centsJson(rolled.valueCents),
                willAutoSell,
                openIndex: i,
                openCount: batchCount,
                unitPrice: centsJson(unitPrice),
                basePrice: centsJson(c.price),
              }),
            },
          });
          createdTxIds.push(openTx.id);

          drops.push({
            inventoryId: inventoryItem.id,
            id: item.id,
            name: item.name,
            imageUrl: item.imageUrl,
            rarity: item.rarity,
            marketValue: toCentsBigInt(item.marketValue).toString(),
            floatValue: rolled.floatValue,
            wear: rolled.wear,
            paintSeed: rolled.paintSeed,
            valueCents: toCentsBigInt(rolled.valueCents).toString(),
            willAutoSell,
          });
        }
        return { drops, createdTxIds };
      }

      let { drops, createdTxIds } = await createDrops(count);

      // Respin: free re-roll after showing the first result.
      // - Auto-sell ON: junk (< threshold) stays for deferred sell; keepers (>= threshold)
      //   stay in inventory; second roll is extra loot.
      // - Auto-sell OFF: classic replace — first roll is discarded entirely.
      let preRespinItems = null;
      let keptPreRespinItems = [];
      const respinChance = Math.max(0, Number(user.prestigeRespinChance) || 0);
      const didRespin = respinChance > 0 && Math.random() * 100 < respinChance;
      if (didRespin) {
        const autoSellOn = Boolean(user.autoSellEnabled);
        const pendingAutoSellIds = drops
          .filter((d) => d.willAutoSell)
          .map((d) => d.inventoryId)
          .filter(Boolean);
        keptPreRespinItems = autoSellOn
          ? drops.filter((d) => !d.willAutoSell && d.inventoryId)
          : [];
        const keepIdSet = new Set([
          ...pendingAutoSellIds,
          ...keptPreRespinItems.map((d) => d.inventoryId),
        ]);
        const discardIds = drops
          .map((d) => d.inventoryId)
          .filter((id) => id && !keepIdSet.has(id));

        preRespinItems = drops.map((d) => ({
          ...d,
          inventoryId: keepIdSet.has(d.inventoryId) ? d.inventoryId : null,
          preRespin: true,
          kept: Boolean(d.inventoryId && keepIdSet.has(d.inventoryId) && !d.willAutoSell),
        }));

        if (discardIds.length) {
          await tx.inventoryItem.deleteMany({ where: { id: { in: discardIds } } });
        }
        // Drop open_case txs for discarded / pending-auto-sell rows; keep txs for keepers.
        const deleteTxIds = drops
          .map((d, i) => (keepIdSet.has(d.inventoryId) && !d.willAutoSell ? null : createdTxIds[i]))
          .filter(Boolean);
        if (deleteTxIds.length) {
          await tx.transaction.deleteMany({ where: { id: { in: deleteTxIds } } });
        }
        const again = await createDrops(count);
        drops = again.drops;
        createdTxIds = again.createdTxIds;
        await tx.transaction.create({
          data: {
            userId,
            type: 'case_respin',
            amount: '0',
            meta: JSON.stringify({
              caseId: c.id,
              openCount: count,
              inventoryItemIds: drops.map((d) => d.inventoryId),
              preRespinPendingAutoSellIds: pendingAutoSellIds,
              preRespinKeptIds: keptPreRespinItems.map((d) => d.inventoryId),
            }),
          },
        });
      }

      // Refund: chance to refund total cost
      const refundChance = Math.max(0, Number(user.prestigeRefundChance) || 0);
      const didRefund = refundChance > 0 && Math.random() * 100 < refundChance;

      const catalog = await fillCatalogSlots(
        tx,
        userId,
        caseId,
        [...keptPreRespinItems.map((d) => d.id), ...drops.map((d) => d.id)]
      );

      const applied = applyOpens(progressRow, count, 0);
      const updatedProgress = await tx.caseKeyProgress.update({
        where: { id: progressRow.id },
        data: {
          keys: applied.keys,
          opensSinceLastKey: applied.opensSinceLastKey,
          totalOpens: applied.totalOpens,
        },
      });

      let updatedUser = await tx.user.findUnique({ where: { id: userId } });
      if (didRefund) {
        updatedUser = await adjustUserBalance(tx, userId, updatedUser.balance, totalCost);
        await tx.transaction.create({
          data: {
            userId,
            type: 'case_refund',
            amount: centsJson(totalCost),
            meta: JSON.stringify({
              caseId: c.id,
              caseName: c.name,
              openCount: count,
              unitPrice: centsJson(unitPrice),
              totalCost: centsJson(totalCost),
            }),
          },
        });
      }

      return {
        user: publicUser(updatedUser),
        caseName: c.name,
        items: drops,
        preRespinItems,
        keptPreRespinItems,
        keyProgress: serializeKeyProgress(updatedProgress, 0, openBonus),
        keysGained: applied.keysGained,
        catalog,
        unitPrice: centsJson(unitPrice),
        totalCost: centsJson(totalCost),
        refunded: didRefund,
        respin: didRespin,
        animReduction: user.prestigeAnimReduction ?? 0,
      };
    });

    checkAchievements(userId).catch((err) => console.error('achievements', err));

    res.json({
      user: result.user,
      caseName: result.caseName,
      count: result.items.length,
      items: result.items,
      preRespinItems: result.preRespinItems,
      keptPreRespinItems: result.keptPreRespinItems,
      keyProgress: result.keyProgress,
      keysGained: result.keysGained,
      catalog: result.catalog,
      item: result.items[0],
      unitPrice: result.unitPrice,
      totalCost: result.totalCost,
      refunded: result.refunded,
      respin: result.respin,
      animReduction: result.animReduction,
    });
  } catch (err) {
    console.error(err);
    const status = err.status || 500;
    res.status(status).json({ error: err.message || 'Failed to open case' });
  }
});

export default router;
