import crypto from "node:crypto";
import { CasinoGameKey, CasinoRoundStatus, LedgerEntryType, Prisma } from "@prisma/client";
import { z } from "zod";
import { writeAuditLog } from "@/server/audit";
import { createClientSeed, createServerSeed, serializeRound, sha256 } from "@/server/casino/dice";
import { prisma } from "@/server/db";
import { ConflictError } from "@/server/errors";
import { creditPayoutByStakeSourceInTx, recordWagerInTx } from "@/server/vip/rewards";
import { debitPlayableBalanceInTx, decimal } from "@/server/wallet/ledger";

const HOUSE_EDGE_PERCENT = decimal(1);
const TILE_COUNT = 25;

export const minesConfig = {
  currency: "EUR",
  minStake: "0.01",
  maxStake: "10000",
  maxPayout: "100000",
  tileCount: TILE_COUNT,
  minMines: 1,
  maxMines: 24,
  houseEdgeBps: 100
};

export const minesStartSchema = z.object({
  currency: z.string().min(3).max(8).default("EUR"),
  stake: z.coerce.number().positive().max(Number(minesConfig.maxStake)),
  mines: z.coerce.number().int().min(minesConfig.minMines).max(minesConfig.maxMines),
  clientRequestId: z.string().uuid().optional()
});

export const minesRevealSchema = z.object({
  roundId: z.string().uuid(),
  tile: z.coerce.number().int().min(0).max(TILE_COUNT - 1)
});

export const minesCashoutSchema = z.object({
  roundId: z.string().uuid()
});

export const minesRotateSeedSchema = z.object({
  clientSeed: z.string().trim().min(4).max(96).optional()
});

export const minesVerifySchema = z.object({
  clientSeed: z.string().min(1).max(160),
  serverSeed: z.string().min(16).max(256),
  nonce: z.coerce.number().int().min(0).max(10_000_000),
  mines: z.coerce.number().int().min(minesConfig.minMines).max(minesConfig.maxMines)
});

type MinesMetadata = {
  tileCount: number;
  mineCount: number;
  mineTiles: number[];
  revealedTiles: number[];
  explodedTile?: number;
  cashedOutAt?: string;
  stakeSplit?: {
    available: string;
    bonus: string;
  };
  ledgerEntryIds?: string[];
};

type MinesRoundShape = {
  id: string;
  userId: string;
  walletId: string;
  game: CasinoGameKey;
  currency: string;
  stake: Prisma.Decimal;
  payout: Prisma.Decimal;
  profit: Prisma.Decimal;
  multiplier: Prisma.Decimal;
  chance: Prisma.Decimal;
  target: Prisma.Decimal;
  direction: string;
  result: Prisma.Decimal;
  nonce: number;
  status: CasinoRoundStatus;
  serverSeedHash: string;
  createdAt: Date;
  metadata: Prisma.JsonValue | null;
};

export type MinesStartInput = z.infer<typeof minesStartSchema>;

export function calculateMinesMultiplier(input: { mines: number; revealed: number }) {
  const safeTiles = TILE_COUNT - input.mines;
  if (input.mines < minesConfig.minMines || input.mines > minesConfig.maxMines) {
    throw new ConflictError("Nombre de mines invalide.", "MINES_INVALID_COUNT");
  }
  if (input.revealed < 0 || input.revealed > safeTiles) {
    throw new ConflictError("Nombre de cases révélées invalide.", "MINES_INVALID_REVEAL_COUNT");
  }
  if (input.revealed === 0) return decimal(1).toDecimalPlaces(6);

  let probability = decimal(1);
  for (let index = 0; index < input.revealed; index += 1) {
    probability = probability.mul(safeTiles - index).div(TILE_COUNT - index);
  }

  return decimal(100).minus(HOUSE_EDGE_PERCENT).div(100).div(probability).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
}

export function calculateMinesChance(input: { mines: number; revealed: number }) {
  const safeTiles = TILE_COUNT - input.mines;
  if (input.revealed >= safeTiles) return decimal(100).toDecimalPlaces(8);
  const remainingTiles = TILE_COUNT - input.revealed;
  const remainingSafe = safeTiles - input.revealed;
  return decimal(remainingSafe).div(remainingTiles).mul(100).toDecimalPlaces(8, Prisma.Decimal.ROUND_DOWN);
}

export function calculateMinesTiles(input: { serverSeed: string; clientSeed: string; nonce: number; mines: number }) {
  const tiles = Array.from({ length: TILE_COUNT }, (_, index) => index);

  for (let index = tiles.length - 1; index > 0; index -= 1) {
    const digest = crypto
      .createHmac("sha256", input.serverSeed)
      .update(`${input.clientSeed}:${input.nonce}:mines:${input.mines}:${index}`)
      .digest("hex");
    const sample = Number(BigInt(`0x${digest.slice(0, 13)}`));
    const swapIndex = sample % (index + 1);
    [tiles[index], tiles[swapIndex]] = [tiles[swapIndex], tiles[index]];
  }

  return tiles.slice(0, input.mines).sort((a, b) => a - b);
}

export async function getMinesState(userId: string, currency: string) {
  const seedPair = await ensureMinesSeedPair(userId);
  const [rounds, activeRound] = await Promise.all([
    prisma.casinoRound.findMany({
      where: { userId, game: CasinoGameKey.MINES, status: { not: CasinoRoundStatus.ACTIVE } },
      orderBy: { createdAt: "desc" },
      take: 25
    }),
    prisma.casinoRound.findFirst({
      where: { userId, game: CasinoGameKey.MINES, status: CasinoRoundStatus.ACTIVE },
      orderBy: { createdAt: "desc" }
    })
  ]);

  const stats = rounds.reduce(
    (acc, round) => {
      acc.stake = acc.stake.plus(round.stake);
      acc.profit = acc.profit.plus(round.profit);
      if (round.status === CasinoRoundStatus.WON) acc.wins += 1;
      if (round.status === CasinoRoundStatus.LOST) acc.losses += 1;
      return acc;
    },
    { stake: decimal(0), profit: decimal(0), wins: 0, losses: 0 }
  );

  return {
    config: minesConfig,
    currency,
    seedPair: {
      id: seedPair.id,
      clientSeed: seedPair.clientSeed,
      serverSeedHash: seedPair.serverSeedHash,
      nonce: seedPair.nonce
    },
    stats: {
      stake: stats.stake.toDecimalPlaces(6).toString(),
      profit: stats.profit.toDecimalPlaces(6).toString(),
      wins: stats.wins,
      losses: stats.losses
    },
    activeRound: activeRound ? serializeMinesRound(activeRound) : null,
    recentRounds: rounds.map((round) => serializeMinesRound(round))
  };
}

export async function startMines(userId: string, input: MinesStartInput) {
  if (input.clientRequestId) {
    const existing = await prisma.casinoRound.findUnique({ where: { clientRequestId: input.clientRequestId } });
    if (existing) {
      if (existing.userId !== userId) {
        throw new ConflictError("Cette requête de jeu existe déjà.", "MINES_REQUEST_ID_CONFLICT");
      }
      return serializeMinesRound(existing);
    }
  }

  return prisma.$transaction(async (tx) => {
    const activeRound = await tx.casinoRound.findFirst({
      where: { userId, game: CasinoGameKey.MINES, status: CasinoRoundStatus.ACTIVE },
      orderBy: { createdAt: "desc" }
    });
    if (activeRound) {
      throw new ConflictError("Une partie Mines est déjà en cours.", "MINES_ACTIVE_ROUND_EXISTS", {
        roundId: activeRound.id
      });
    }

    const roundId = crypto.randomUUID();
    const stake = decimal(input.stake).toDecimalPlaces(6);
    if (stake.lt(minesConfig.minStake)) {
      throw new ConflictError(`La mise minimum est de ${minesConfig.minStake} €.`, "MINES_MIN_STAKE");
    }

    const seedPair = await ensureMinesSeedPairInTx(tx, userId);
    await tx.$queryRaw`SELECT id FROM "CasinoSeedPair" WHERE id = CAST(${seedPair.id} AS uuid) FOR UPDATE`;
    const lockedSeedPair = await tx.casinoSeedPair.findUniqueOrThrow({ where: { id: seedPair.id } });
    const nonce = lockedSeedPair.nonce;
    const mineTiles = calculateMinesTiles({
      serverSeed: lockedSeedPair.serverSeed,
      clientSeed: lockedSeedPair.clientSeed,
      nonce,
      mines: input.mines
    });

    const firstMultiplier = calculateMinesMultiplier({ mines: input.mines, revealed: 0 });
    const firstChance = calculateMinesChance({ mines: input.mines, revealed: 0 });

    const { wallet, entries, availableDebit, bonusDebit } = await debitPlayableBalanceInTx(tx, {
      userId,
      currency: input.currency,
      amount: stake,
      type: LedgerEntryType.CASINO_STAKE,
      referenceType: "casino_round",
      referenceId: roundId,
      idempotencyKey: input.clientRequestId ? `casino:mines:${input.clientRequestId}:stake` : undefined,
      reason: "Mines stake",
      metadata: {
        game: "mines",
        mines: input.mines
      }
    });

    const round = await tx.casinoRound.create({
      data: {
        id: roundId,
        userId,
        walletId: wallet.id,
        seedPairId: lockedSeedPair.id,
        game: CasinoGameKey.MINES,
        clientRequestId: input.clientRequestId,
        currency: input.currency,
        stake,
        payout: decimal(0),
        profit: decimal(0),
        multiplier: firstMultiplier,
        chance: firstChance,
        target: decimal(input.mines),
        direction: "mines",
        result: decimal(0),
        nonce,
        status: CasinoRoundStatus.ACTIVE,
        serverSeedHash: lockedSeedPair.serverSeedHash,
        metadata: {
          tileCount: TILE_COUNT,
          mineCount: input.mines,
          mineTiles,
          revealedTiles: [],
          stakeSplit: {
            available: availableDebit.toString(),
            bonus: bonusDebit.toString()
          },
          ledgerEntryIds: entries.map((entry) => entry.id)
        }
      }
    });

    await tx.casinoSeedPair.update({
      where: { id: lockedSeedPair.id },
      data: { nonce: { increment: 1 } }
    });

    await recordWagerInTx(tx, {
      userId,
      currency: input.currency,
      stake,
      bonusDebit,
      referenceType: "casino_round",
      referenceId: round.id
    });

    await writeAuditLog(
      {
        actorUserId: userId,
        action: "casino.mines.start",
        entityType: "CasinoRound",
        entityId: round.id,
        after: {
          stake: stake.toString(),
          mines: input.mines,
          serverSeedHash: lockedSeedPair.serverSeedHash
        }
      },
      tx
    );

    return serializeMinesRound(round);
  }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
}

export async function revealMinesTile(userId: string, input: z.infer<typeof minesRevealSchema>) {
  return prisma.$transaction(async (tx) => {
    await tx.$queryRaw`SELECT id FROM "CasinoRound" WHERE id = CAST(${input.roundId} AS uuid) FOR UPDATE`;
    const round = await tx.casinoRound.findUniqueOrThrow({ where: { id: input.roundId } });
    assertMinesOwnerAndActive(round, userId);

    const metadata = parseMinesMetadata(round.metadata);
    if (metadata.revealedTiles.includes(input.tile)) {
      throw new ConflictError("Cette case est déjà révélée.", "MINES_TILE_ALREADY_REVEALED");
    }

    if (metadata.mineTiles.includes(input.tile)) {
      const updated = await tx.casinoRound.update({
        where: { id: round.id },
        data: {
          status: CasinoRoundStatus.LOST,
          profit: round.stake.negated().toDecimalPlaces(6),
          multiplier: decimal(0),
          chance: decimal(0),
          result: decimal(input.tile),
          metadata: {
            ...metadata,
            explodedTile: input.tile,
            revealedTiles: [...metadata.revealedTiles, input.tile]
          }
        }
      });

      await writeAuditLog(
        {
          actorUserId: userId,
          action: "casino.mines.reveal_mine",
          entityType: "CasinoRound",
          entityId: round.id,
          after: { tile: input.tile, status: CasinoRoundStatus.LOST }
        },
        tx
      );

      return serializeMinesRound(updated);
    }

    const revealedTiles = [...metadata.revealedTiles, input.tile].sort((a, b) => a - b);
    const multiplier = calculateMinesMultiplier({ mines: metadata.mineCount, revealed: revealedTiles.length });
    const chance = calculateMinesChance({ mines: metadata.mineCount, revealed: revealedTiles.length });
    const safeTiles = TILE_COUNT - metadata.mineCount;

    if (revealedTiles.length >= safeTiles) {
      return settleMinesCashout(tx, round, { ...metadata, revealedTiles }, userId, "auto_complete");
    }

    const updated = await tx.casinoRound.update({
      where: { id: round.id },
      data: {
        multiplier,
        chance,
        result: decimal(revealedTiles.length),
        metadata: {
          ...metadata,
          revealedTiles
        }
      }
    });

    return serializeMinesRound(updated);
  }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
}

export async function cashoutMines(userId: string, input: z.infer<typeof minesCashoutSchema>) {
  return prisma.$transaction(async (tx) => {
    await tx.$queryRaw`SELECT id FROM "CasinoRound" WHERE id = CAST(${input.roundId} AS uuid) FOR UPDATE`;
    const round = await tx.casinoRound.findUniqueOrThrow({ where: { id: input.roundId } });

    if (round.userId !== userId || round.game !== CasinoGameKey.MINES) {
      throw new ConflictError("Partie Mines introuvable.", "MINES_ROUND_NOT_FOUND");
    }
    if (round.status === CasinoRoundStatus.WON) return serializeMinesRound(round);
    if (round.status !== CasinoRoundStatus.ACTIVE) {
      throw new ConflictError("Cette partie Mines n'est plus encaissable.", "MINES_ROUND_NOT_ACTIVE");
    }

    const metadata = parseMinesMetadata(round.metadata);
    if (metadata.revealedTiles.length === 0) {
      throw new ConflictError("Révèle au moins une case avant d'encaisser.", "MINES_NO_TILE_REVEALED");
    }

    return settleMinesCashout(tx, round, metadata, userId, "cashout");
  }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
}

export async function rotateMinesSeed(userId: string, input: z.infer<typeof minesRotateSeedSchema>) {
  return prisma.$transaction(async (tx) => {
    const current = await ensureMinesSeedPairInTx(tx, userId);
    await tx.$queryRaw`SELECT id FROM "CasinoSeedPair" WHERE id = CAST(${current.id} AS uuid) FOR UPDATE`;

    const serverSeed = createServerSeed();
    const clientSeed = input.clientSeed?.trim() || createClientSeed();
    const next = await tx.casinoSeedPair.create({
      data: {
        userId,
        game: CasinoGameKey.MINES,
        clientSeed,
        serverSeed,
        serverSeedHash: sha256(serverSeed)
      }
    });

    await tx.casinoSeedPair.update({
      where: { id: current.id },
      data: { active: false, rotatedAt: new Date() }
    });

    return {
      previous: {
        id: current.id,
        clientSeed: current.clientSeed,
        serverSeed: current.serverSeed,
        serverSeedHash: current.serverSeedHash,
        nonce: current.nonce
      },
      current: {
        id: next.id,
        clientSeed: next.clientSeed,
        serverSeedHash: next.serverSeedHash,
        nonce: next.nonce
      }
    };
  });
}

export function verifyMines(input: z.infer<typeof minesVerifySchema>) {
  return {
    serverSeedHash: sha256(input.serverSeed),
    mineTiles: calculateMinesTiles(input)
  };
}

async function settleMinesCashout(
  tx: Prisma.TransactionClient,
  round: MinesRoundShape,
  metadata: MinesMetadata,
  userId: string,
  mode: "cashout" | "auto_complete"
) {
  const multiplier = calculateMinesMultiplier({ mines: metadata.mineCount, revealed: metadata.revealedTiles.length });
  const rawPayout = round.stake.mul(multiplier).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
  const payout = rawPayout.gt(minesConfig.maxPayout) ? decimal(minesConfig.maxPayout) : rawPayout;
  const profit = payout.minus(round.stake).toDecimalPlaces(6);
  const payoutEntries = await creditPayoutByStakeSourceInTx(tx, {
    userId,
    walletId: round.walletId,
    currency: round.currency,
    stake: round.stake,
    payout,
    availableDebit: metadata.stakeSplit?.available ?? 0,
    bonusDebit: metadata.stakeSplit?.bonus ?? 0,
    type: LedgerEntryType.CASINO_WIN,
    referenceType: "casino_round",
    referenceId: round.id,
    idempotencyKey: `casino:mines:${round.id}:cashout`,
    reason: "Mines cashout",
    metadata: {
      game: "mines",
      mode,
      revealedTiles: metadata.revealedTiles.length,
      multiplier: multiplier.toString()
    }
  });

  const updated = await tx.casinoRound.update({
    where: { id: round.id },
    data: {
      status: CasinoRoundStatus.WON,
      payout,
      profit,
      multiplier,
      chance: decimal(100),
      result: decimal(metadata.revealedTiles.length),
      metadata: {
        ...metadata,
        cashedOutAt: new Date().toISOString(),
        ledgerEntryIds: [...(metadata.ledgerEntryIds ?? []), ...payoutEntries.map((entry) => entry.id)]
      }
    }
  });

  await writeAuditLog(
    {
      actorUserId: userId,
      action: "casino.mines.cashout",
      entityType: "CasinoRound",
      entityId: round.id,
      after: {
        payout: payout.toString(),
        profit: profit.toString(),
        multiplier: multiplier.toString(),
        mode
      }
    },
    tx
  );

  return serializeMinesRound(updated);
}

async function ensureMinesSeedPair(userId: string) {
  return prisma.$transaction((tx) => ensureMinesSeedPairInTx(tx, userId));
}

async function ensureMinesSeedPairInTx(tx: Prisma.TransactionClient, userId: string) {
  const existing = await tx.casinoSeedPair.findFirst({
    where: { userId, game: CasinoGameKey.MINES, active: true },
    orderBy: { createdAt: "desc" }
  });

  if (existing) {
    return existing;
  }

  const serverSeed = createServerSeed();
  return tx.casinoSeedPair.create({
    data: {
      userId,
      game: CasinoGameKey.MINES,
      clientSeed: createClientSeed(),
      serverSeed,
      serverSeedHash: sha256(serverSeed)
    }
  });
}

function assertMinesOwnerAndActive(round: MinesRoundShape, userId: string) {
  if (round.userId !== userId || round.game !== CasinoGameKey.MINES) {
    throw new ConflictError("Partie Mines introuvable.", "MINES_ROUND_NOT_FOUND");
  }
  if (round.status !== CasinoRoundStatus.ACTIVE) {
    throw new ConflictError("Cette partie Mines est terminée.", "MINES_ROUND_NOT_ACTIVE");
  }
}

function parseMinesMetadata(value: Prisma.JsonValue | null): MinesMetadata {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new ConflictError("Métadonnées Mines invalides.", "MINES_INVALID_METADATA");
  }
  const metadata = value as Partial<MinesMetadata>;
  if (!Array.isArray(metadata.mineTiles) || !Array.isArray(metadata.revealedTiles) || typeof metadata.mineCount !== "number") {
    throw new ConflictError("Métadonnées Mines invalides.", "MINES_INVALID_METADATA");
  }

  return {
    tileCount: metadata.tileCount ?? TILE_COUNT,
    mineCount: metadata.mineCount,
    mineTiles: metadata.mineTiles.map(Number),
    revealedTiles: metadata.revealedTiles.map(Number),
    explodedTile: typeof metadata.explodedTile === "number" ? metadata.explodedTile : undefined,
    cashedOutAt: metadata.cashedOutAt,
    stakeSplit: metadata.stakeSplit,
    ledgerEntryIds: metadata.ledgerEntryIds
  };
}

function serializeMinesRound(round: MinesRoundShape) {
  const metadata = parseMinesMetadata(round.metadata);
  const revealedCount = metadata.revealedTiles.filter((tile) => !metadata.mineTiles.includes(tile)).length;
  const currentMultiplier = calculateMinesMultiplier({ mines: metadata.mineCount, revealed: revealedCount });
  const nextRevealed = Math.min(TILE_COUNT - metadata.mineCount, revealedCount + 1);
  const nextMultiplier = calculateMinesMultiplier({ mines: metadata.mineCount, revealed: nextRevealed });
  const publicRound = serializeRound(round);
  const visibleMines = round.status === CasinoRoundStatus.ACTIVE ? [] : metadata.mineTiles;
  const rawPotentialPayout = round.stake.mul(currentMultiplier).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
  const potentialPayout = rawPotentialPayout.gt(minesConfig.maxPayout) ? decimal(minesConfig.maxPayout) : rawPotentialPayout;

  return {
    ...publicRound,
    tileCount: metadata.tileCount,
    mineCount: metadata.mineCount,
    revealedTiles: metadata.revealedTiles,
    mineTiles: visibleMines,
    explodedTile: metadata.explodedTile,
    currentMultiplier: currentMultiplier.toString(),
    nextMultiplier: nextMultiplier.toString(),
    potentialPayout: potentialPayout.toString(),
    canCashout: round.status === CasinoRoundStatus.ACTIVE && revealedCount > 0
  };
}
