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";

export const blackjackConfig = {
  currency: "EUR",
  minStake: "0.01",
  maxStake: "10000",
  maxSideBet: "1000",
  maxPayout: "100000",
  blackjackPayout: "2.5",
  houseEdgeBps: 52,
  sideBets: {
    perfectPairs: {
      mixedPair: "6",
      coloredPair: "13",
      perfectPair: "26"
    },
    twentyOneThree: {
      flush: "6",
      straight: "11",
      threeOfAKind: "31",
      straightFlush: "41",
      suitedTrips: "101"
    }
  }
};

export const blackjackStartSchema = z.object({
  currency: z.string().min(3).max(8).default("EUR"),
  stake: z.coerce.number().positive().max(Number(blackjackConfig.maxStake)),
  sideBets: z
    .object({
      perfectPairs: z.coerce.number().min(0).max(Number(blackjackConfig.maxSideBet)).default(0),
      twentyOneThree: z.coerce.number().min(0).max(Number(blackjackConfig.maxSideBet)).default(0)
    })
    .optional(),
  clientRequestId: z.string().uuid().optional()
});

export const blackjackActionSchema = z.object({
  roundId: z.string().uuid(),
  action: z.enum(["hit", "stand", "double", "split"])
});

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

export const blackjackVerifySchema = 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)
});

export type BlackjackStartInput = z.infer<typeof blackjackStartSchema>;
type BlackjackActionInput = z.infer<typeof blackjackActionSchema>;

type Suit = "spades" | "hearts" | "diamonds" | "clubs";
type Rank = "A" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "J" | "Q" | "K";
type HandStatus = "PLAYING" | "STAND" | "BUST" | "BLACKJACK" | "WON" | "LOST" | "PUSH";

export type BlackjackCard = {
  suit: Suit;
  rank: Rank;
  code: string;
};

type PublicBlackjackCard = BlackjackCard & { hidden?: boolean };

type BlackjackHand = {
  id: string;
  cards: BlackjackCard[];
  stake: string;
  status: HandStatus;
  doubled?: boolean;
  payout?: string;
  profit?: string;
  result?: "win" | "loss" | "push" | "bust" | "blackjack";
};

type BlackjackSideBet = {
  type: "perfectPairs" | "twentyOneThree";
  label: string;
  selection: string;
  stake: string;
  multiplier: string;
  payout: string;
  profit: string;
  status: "WON" | "LOST";
  cards: string[];
};

type BlackjackMetadata = {
  deck: BlackjackCard[];
  dealerCards: BlackjackCard[];
  hands: BlackjackHand[];
  activeHandIndex: number;
  sideBets?: BlackjackSideBet[];
  settledAt?: string;
  dealerResult?: "stand" | "bust" | "blackjack";
  stakeSplit?: {
    available: string;
    bonus: string;
  };
  ledgerEntryIds?: string[];
};

type BlackjackRoundShape = {
  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;
};

const suits: Suit[] = ["spades", "hearts", "diamonds", "clubs"];
const ranks: Rank[] = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];

export function calculateBlackjackDeck(input: { serverSeed: string; clientSeed: string; nonce: number }) {
  const deck = createDeck();

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

  return deck;
}

export function blackjackValue(cards: BlackjackCard[]) {
  let total = 0;
  let aces = 0;

  for (const card of cards) {
    if (card.rank === "A") {
      total += 11;
      aces += 1;
    } else if (["K", "Q", "J"].includes(card.rank)) {
      total += 10;
    } else {
      total += Number(card.rank);
    }
  }

  while (total > 21 && aces > 0) {
    total -= 10;
    aces -= 1;
  }

  return {
    total,
    soft: aces > 0,
    blackjack: cards.length === 2 && total === 21,
    bust: total > 21
  };
}

export async function getBlackjackState(userId: string, currency: string) {
  const seedPair = await ensureBlackjackSeedPair(userId);
  const [rounds, activeRound] = await Promise.all([
    prisma.casinoRound.findMany({
      where: { userId, game: CasinoGameKey.BLACKJACK, status: { not: CasinoRoundStatus.ACTIVE } },
      orderBy: { createdAt: "desc" },
      take: 25
    }),
    prisma.casinoRound.findFirst({
      where: { userId, game: CasinoGameKey.BLACKJACK, 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: blackjackConfig,
    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 ? serializeBlackjackRound(activeRound) : null,
    recentRounds: rounds.map((round) => serializeBlackjackRound(round))
  };
}

export async function startBlackjack(userId: string, input: BlackjackStartInput) {
  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à.", "BLACKJACK_REQUEST_ID_CONFLICT");
      }
      return serializeBlackjackRound(existing);
    }
  }

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

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

    const seedPair = await ensureBlackjackSeedPairInTx(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 shuffledDeck = calculateBlackjackDeck({
      serverSeed: lockedSeedPair.serverSeed,
      clientSeed: lockedSeedPair.clientSeed,
      nonce
    });

    const playerCards = [shuffledDeck[0], shuffledDeck[2]];
    const dealerCards = [shuffledDeck[1], shuffledDeck[3]];
    const playerValue = blackjackValue(playerCards);
    const dealerValue = blackjackValue(dealerCards);
    let deck = shuffledDeck.slice(4);
    const sideBets = resolveSideBets(input.sideBets, playerCards, dealerCards[0]);
    const sideBetStake = totalSideBetStake(sideBets);
    const initialStake = stake.plus(sideBetStake).toDecimalPlaces(6);

    const metadata: BlackjackMetadata = {
      deck,
      dealerCards,
      hands: [
        {
          id: crypto.randomUUID(),
          cards: playerCards,
          stake: stake.toString(),
          status: playerValue.blackjack ? "BLACKJACK" : "PLAYING"
        }
      ],
      activeHandIndex: 0,
      sideBets
    };

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

    metadata.stakeSplit = {
      available: availableDebit.toString(),
      bonus: bonusDebit.toString()
    };
    metadata.ledgerEntryIds = entries.map((entry) => entry.id);

    let status: CasinoRoundStatus = CasinoRoundStatus.ACTIVE;
    let payout = decimal(0);
    let profit = decimal(0);
    let multiplier = decimal(0);
    let result = decimal(playerValue.total);
    let target = decimal(21);

    if (playerValue.blackjack || dealerValue.blackjack) {
      metadata.dealerCards = dealerCards;
      metadata.activeHandIndex = -1;
      metadata.settledAt = new Date().toISOString();
      metadata.dealerResult = dealerValue.blackjack ? "blackjack" : "stand";
      const sideBetPayout = totalSideBetPayout(sideBets);
      const sideBetProfit = sideBetPayout.minus(sideBetStake).toDecimalPlaces(6);
      let mainPayout = decimal(0);
      let mainProfit = stake.negated().toDecimalPlaces(6);

      if (playerValue.blackjack && dealerValue.blackjack) {
        mainPayout = stake;
        mainProfit = decimal(0);
        metadata.hands[0].status = "PUSH";
        metadata.hands[0].payout = mainPayout.toString();
        metadata.hands[0].profit = mainProfit.toString();
        metadata.hands[0].result = "push";
      } else if (playerValue.blackjack) {
        mainPayout = stake.mul(blackjackConfig.blackjackPayout).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
        mainProfit = mainPayout.minus(stake).toDecimalPlaces(6);
        metadata.hands[0].status = "WON";
        metadata.hands[0].payout = mainPayout.toString();
        metadata.hands[0].profit = mainProfit.toString();
        metadata.hands[0].result = "blackjack";
      } else {
        metadata.hands[0].status = "LOST";
        metadata.hands[0].payout = "0";
        metadata.hands[0].profit = mainProfit.toString();
        metadata.hands[0].result = "loss";
      }

      payout = mainPayout.plus(sideBetPayout).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
      profit = mainProfit.plus(sideBetProfit).toDecimalPlaces(6);
      status = profit.gt(0) ? CasinoRoundStatus.WON : profit.lt(0) ? CasinoRoundStatus.LOST : CasinoRoundStatus.VOID;

      if (payout.gt(0)) {
        const payoutEntries = await creditPayoutByStakeSourceInTx(tx, {
          userId,
          walletId: wallet.id,
          currency: input.currency,
          stake: initialStake,
          payout,
          availableDebit,
          bonusDebit,
          type: LedgerEntryType.CASINO_WIN,
          referenceType: "casino_round",
          referenceId: roundId,
          idempotencyKey: input.clientRequestId ? `casino:blackjack:${input.clientRequestId}:win` : undefined,
          reason: "Blackjack win",
          metadata: {
            game: "blackjack",
            result: metadata.hands[0].result,
            sideBets: metadata.sideBets
          }
        });
        metadata.ledgerEntryIds = [...(metadata.ledgerEntryIds ?? []), ...payoutEntries.map((entry) => entry.id)];
      }

      multiplier = payout.gt(0) ? payout.div(initialStake).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN) : decimal(0);
      result = decimal(playerValue.total);
      target = decimal(dealerValue.total);
    }

    const round = await tx.casinoRound.create({
      data: {
        id: roundId,
        userId,
        walletId: wallet.id,
        seedPairId: lockedSeedPair.id,
        game: CasinoGameKey.BLACKJACK,
        clientRequestId: input.clientRequestId,
        currency: input.currency,
        stake: initialStake,
        payout,
        profit,
        multiplier,
        chance: decimal(0),
        target,
        direction: "blackjack",
        result,
        nonce,
        status,
        serverSeedHash: lockedSeedPair.serverSeedHash,
        metadata: toJson(metadata)
      }
    });

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

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

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

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

export async function actBlackjack(userId: string, input: BlackjackActionInput) {
  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 } });
    assertBlackjackOwnerAndActive(round, userId);

    const metadata = parseBlackjackMetadata(round.metadata);
    const hand = metadata.hands[metadata.activeHandIndex];
    if (!hand || hand.status !== "PLAYING") {
      throw new ConflictError("Aucune main Blackjack active.", "BLACKJACK_NO_ACTIVE_HAND");
    }

    if (input.action === "hit") {
      dealToHand(metadata, hand);
      const value = blackjackValue(hand.cards);
      if (value.bust) {
        hand.status = "BUST";
        hand.result = "bust";
      } else if (value.total === 21) {
        hand.status = "STAND";
      }
    }

    if (input.action === "stand") {
      hand.status = "STAND";
    }

    if (input.action === "double") {
      if (!canDouble(hand)) {
        throw new ConflictError("Cette main ne peut pas être doublée.", "BLACKJACK_DOUBLE_NOT_ALLOWED");
      }
      await debitAdditionalStake(tx, round, metadata, decimal(hand.stake), "double");
      hand.stake = decimal(hand.stake).mul(2).toDecimalPlaces(6).toString();
      hand.doubled = true;
      dealToHand(metadata, hand);
      const value = blackjackValue(hand.cards);
      hand.status = value.bust ? "BUST" : "STAND";
      if (value.bust) hand.result = "bust";
    }

    if (input.action === "split") {
      if (!canSplit(hand) || metadata.hands.length > 1) {
        throw new ConflictError("Cette main ne peut pas être splittée.", "BLACKJACK_SPLIT_NOT_ALLOWED");
      }
      await debitAdditionalStake(tx, round, metadata, decimal(hand.stake), "split");
      const [firstCard, secondCard] = hand.cards;
      const first = drawOne(metadata);
      const second = drawOne(metadata);
      metadata.hands = [
        {
          id: hand.id,
          cards: [firstCard, first],
          stake: hand.stake,
          status: "PLAYING"
        },
        {
          id: crypto.randomUUID(),
          cards: [secondCard, second],
          stake: hand.stake,
          status: "PLAYING"
        }
      ];
      metadata.activeHandIndex = 0;
    } else {
      advanceActiveHand(metadata);
    }

    const hasActiveHand = metadata.hands.some((item) => item.status === "PLAYING");
    if (!hasActiveHand) {
      return settleBlackjackRound(tx, round, metadata, userId);
    }

    const updated = await tx.casinoRound.update({
      where: { id: round.id },
      data: {
        stake: totalRoundStake(metadata),
        result: decimal(Math.max(...metadata.hands.map((item) => blackjackValue(item.cards).total))).toDecimalPlaces(6),
        metadata: toJson(metadata)
      }
    });

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

export async function rotateBlackjackSeed(userId: string, input: z.infer<typeof blackjackRotateSeedSchema>) {
  return prisma.$transaction(async (tx) => {
    const current = await ensureBlackjackSeedPairInTx(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.BLACKJACK,
        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 verifyBlackjack(input: z.infer<typeof blackjackVerifySchema>) {
  const deck = calculateBlackjackDeck(input);
  return {
    serverSeedHash: sha256(input.serverSeed),
    openingDeal: {
      player: [deck[0], deck[2]],
      dealer: [deck[1], deck[3]]
    },
    deck
  };
}

async function settleBlackjackRound(
  tx: Prisma.TransactionClient,
  round: BlackjackRoundShape,
  metadata: BlackjackMetadata,
  userId: string
) {
  let dealerCards = [...metadata.dealerCards];
  let dealerValue = blackjackValue(dealerCards);

  while (!dealerValue.bust && dealerValue.total < 17) {
    const card = drawOne(metadata);
    dealerCards = [...dealerCards, card];
    dealerValue = blackjackValue(dealerCards);
  }

  metadata.dealerCards = dealerCards;
  metadata.dealerResult = dealerValue.bust ? "bust" : dealerValue.blackjack ? "blackjack" : "stand";
  metadata.activeHandIndex = -1;
  metadata.settledAt = new Date().toISOString();

  let totalPayout = decimal(0);
  let totalStake = totalSideBetStake(metadata.sideBets ?? []);
  totalPayout = totalPayout.plus(totalSideBetPayout(metadata.sideBets ?? []));

  metadata.hands = metadata.hands.map((hand) => {
    const stake = decimal(hand.stake);
    const value = blackjackValue(hand.cards);
    totalStake = totalStake.plus(stake);

    let payout = decimal(0);
    let result: BlackjackHand["result"] = "loss";
    let status: HandStatus = "LOST";

    if (value.bust || hand.status === "BUST") {
      result = "bust";
      status = "LOST";
    } else if (dealerValue.bust || value.total > dealerValue.total) {
      payout = stake.mul(2);
      result = "win";
      status = "WON";
    } else if (value.total === dealerValue.total) {
      payout = stake;
      result = "push";
      status = "PUSH";
    }

    const profit = payout.minus(stake).toDecimalPlaces(6);
    totalPayout = totalPayout.plus(payout);

    return {
      ...hand,
      status,
      result,
      payout: payout.toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN).toString(),
      profit: profit.toString()
    };
  });

  const cappedPayout = totalPayout.gt(blackjackConfig.maxPayout) ? decimal(blackjackConfig.maxPayout) : totalPayout;
  const profit = cappedPayout.minus(totalStake).toDecimalPlaces(6);
  const roundStatus = profit.gt(0) ? CasinoRoundStatus.WON : profit.lt(0) ? CasinoRoundStatus.LOST : CasinoRoundStatus.VOID;

  if (cappedPayout.gt(0)) {
    const payoutEntries = await creditPayoutByStakeSourceInTx(tx, {
      userId,
      walletId: round.walletId,
      currency: round.currency,
      stake: totalStake,
      payout: cappedPayout,
      availableDebit: metadata.stakeSplit?.available ?? 0,
      bonusDebit: metadata.stakeSplit?.bonus ?? 0,
      type: LedgerEntryType.CASINO_WIN,
      referenceType: "casino_round",
      referenceId: round.id,
      idempotencyKey: `casino:blackjack:${round.id}:settle`,
      reason: "Blackjack settlement",
      metadata: {
        game: "blackjack",
        dealerTotal: dealerValue.total
      }
    });
    metadata.ledgerEntryIds = [...(metadata.ledgerEntryIds ?? []), ...payoutEntries.map((entry) => entry.id)];
  }

  const bestPlayerTotal = Math.max(...metadata.hands.map((hand) => blackjackValue(hand.cards).total));
  const updated = await tx.casinoRound.update({
    where: { id: round.id },
    data: {
      status: roundStatus,
      stake: totalStake.toDecimalPlaces(6),
      payout: cappedPayout.toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN),
      profit,
      multiplier: totalStake.gt(0) ? cappedPayout.div(totalStake).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN) : decimal(0),
      target: decimal(dealerValue.total),
      result: decimal(bestPlayerTotal),
      metadata: toJson(metadata)
    }
  });

  await writeAuditLog(
    {
      actorUserId: userId,
      action: "casino.blackjack.settle",
      entityType: "CasinoRound",
      entityId: round.id,
      after: {
        status: roundStatus,
        payout: cappedPayout.toString(),
        profit: profit.toString(),
        dealerTotal: dealerValue.total
      }
    },
    tx
  );

  return serializeBlackjackRound(updated);
}

async function debitAdditionalStake(
  tx: Prisma.TransactionClient,
  round: BlackjackRoundShape,
  metadata: BlackjackMetadata,
  amount: Prisma.Decimal,
  mode: "double" | "split"
) {
  const { entries, availableDebit, bonusDebit } = await debitPlayableBalanceInTx(tx, {
    userId: round.userId,
    currency: round.currency,
    amount,
    type: LedgerEntryType.CASINO_STAKE,
    referenceType: "casino_round",
    referenceId: round.id,
    idempotencyKey: `casino:blackjack:${round.id}:${mode}:${metadata.hands.length}`,
    reason: `Blackjack ${mode}`,
    metadata: {
      game: "blackjack",
      mode
    }
  });

  metadata.ledgerEntryIds = [...(metadata.ledgerEntryIds ?? []), ...entries.map((entry) => entry.id)];
  metadata.stakeSplit = {
    available: decimal(metadata.stakeSplit?.available ?? 0).plus(availableDebit).toString(),
    bonus: decimal(metadata.stakeSplit?.bonus ?? 0).plus(bonusDebit).toString()
  };

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

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

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

  if (existing) {
    return existing;
  }

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

function createDeck() {
  return suits.flatMap((suit) =>
    ranks.map((rank) => ({
      suit,
      rank,
      code: `${rank}${suit[0].toUpperCase()}`
    }))
  );
}

function drawOne(metadata: BlackjackMetadata) {
  const card = metadata.deck.shift();
  if (!card) {
    throw new ConflictError("Deck Blackjack épuisé.", "BLACKJACK_DECK_EMPTY");
  }
  return card;
}

function dealToHand(metadata: BlackjackMetadata, hand: BlackjackHand) {
  hand.cards = [...hand.cards, drawOne(metadata)];
}

function advanceActiveHand(metadata: BlackjackMetadata) {
  const nextIndex = metadata.hands.findIndex((hand, index) => index > metadata.activeHandIndex && hand.status === "PLAYING");
  metadata.activeHandIndex = nextIndex;
}

function canDouble(hand: BlackjackHand) {
  return hand.cards.length === 2 && hand.status === "PLAYING" && !hand.doubled;
}

function canSplit(hand: BlackjackHand) {
  if (hand.cards.length !== 2 || hand.status !== "PLAYING") return false;
  return cardBlackjackRankValue(hand.cards[0]) === cardBlackjackRankValue(hand.cards[1]);
}

function cardBlackjackRankValue(card: BlackjackCard) {
  if (card.rank === "A") return 11;
  if (["K", "Q", "J"].includes(card.rank)) return 10;
  return Number(card.rank);
}

function resolveSideBets(
  input: BlackjackStartInput["sideBets"],
  playerCards: BlackjackCard[],
  dealerUpcard: BlackjackCard
): BlackjackSideBet[] {
  const bets: BlackjackSideBet[] = [];
  const perfectPairsStake = decimal(input?.perfectPairs ?? 0).toDecimalPlaces(6);
  const twentyOneThreeStake = decimal(input?.twentyOneThree ?? 0).toDecimalPlaces(6);

  if (perfectPairsStake.gt(0)) {
    bets.push(buildSideBet("perfectPairs", perfectPairsStake, evaluatePerfectPairs(playerCards), playerCards));
  }

  if (twentyOneThreeStake.gt(0)) {
    const cards = [...playerCards, dealerUpcard];
    bets.push(buildSideBet("twentyOneThree", twentyOneThreeStake, evaluateTwentyOneThree(cards), cards));
  }

  return bets;
}

function buildSideBet(
  type: BlackjackSideBet["type"],
  stake: Prisma.Decimal,
  result: { selection: string; multiplier: Prisma.Decimal },
  cards: BlackjackCard[]
): BlackjackSideBet {
  const payout = stake.mul(result.multiplier).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
  return {
    type,
    label: type === "perfectPairs" ? "Paires parfaites" : "21 + 3",
    selection: result.selection,
    stake: stake.toString(),
    multiplier: result.multiplier.toString(),
    payout: payout.toString(),
    profit: payout.minus(stake).toDecimalPlaces(6).toString(),
    status: payout.gt(0) ? "WON" : "LOST",
    cards: cards.map((card) => card.code)
  };
}

function evaluatePerfectPairs(cards: BlackjackCard[]) {
  const [first, second] = cards;
  if (!first || !second || first.rank !== second.rank) {
    return { selection: "Aucune paire", multiplier: decimal(0) };
  }
  if (first.suit === second.suit) {
    return { selection: "Paire parfaite", multiplier: decimal(blackjackConfig.sideBets.perfectPairs.perfectPair) };
  }
  if (isRedSuit(first.suit) === isRedSuit(second.suit)) {
    return { selection: "Paire couleur", multiplier: decimal(blackjackConfig.sideBets.perfectPairs.coloredPair) };
  }
  return { selection: "Paire mixte", multiplier: decimal(blackjackConfig.sideBets.perfectPairs.mixedPair) };
}

function evaluateTwentyOneThree(cards: BlackjackCard[]) {
  const uniqueSuits = new Set(cards.map((card) => card.suit));
  const uniqueRanks = new Set(cards.map((card) => card.rank));
  const flush = uniqueSuits.size === 1;
  const threeOfAKind = uniqueRanks.size === 1;
  const straight = isThreeCardStraight(cards);

  if (flush && threeOfAKind) {
    return { selection: "Brelan assorti", multiplier: decimal(blackjackConfig.sideBets.twentyOneThree.suitedTrips) };
  }
  if (flush && straight) {
    return { selection: "Quinte flush", multiplier: decimal(blackjackConfig.sideBets.twentyOneThree.straightFlush) };
  }
  if (threeOfAKind) {
    return { selection: "Brelan", multiplier: decimal(blackjackConfig.sideBets.twentyOneThree.threeOfAKind) };
  }
  if (straight) {
    return { selection: "Quinte", multiplier: decimal(blackjackConfig.sideBets.twentyOneThree.straight) };
  }
  if (flush) {
    return { selection: "Couleur", multiplier: decimal(blackjackConfig.sideBets.twentyOneThree.flush) };
  }
  return { selection: "Aucune combinaison", multiplier: decimal(0) };
}

function isThreeCardStraight(cards: BlackjackCard[]) {
  const values = cards
    .flatMap((card) => (card.rank === "A" ? [1, 14] : [rankOrder(card.rank)]))
    .filter((value) => value > 0);
  const options = new Set(values);
  for (const start of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) {
    if (options.has(start) && options.has(start + 1) && options.has(start + 2)) return true;
  }
  return false;
}

function rankOrder(rank: Rank) {
  if (rank === "J") return 11;
  if (rank === "Q") return 12;
  if (rank === "K") return 13;
  if (rank === "A") return 14;
  return Number(rank);
}

function isRedSuit(suit: Suit) {
  return suit === "hearts" || suit === "diamonds";
}

function totalHandStake(metadata: BlackjackMetadata) {
  return metadata.hands.reduce((total, hand) => total.plus(hand.stake), decimal(0)).toDecimalPlaces(6);
}

function totalSideBetStake(sideBets: BlackjackSideBet[]) {
  return sideBets.reduce((total, sideBet) => total.plus(sideBet.stake), decimal(0)).toDecimalPlaces(6);
}

function totalSideBetPayout(sideBets: BlackjackSideBet[]) {
  return sideBets.reduce((total, sideBet) => total.plus(sideBet.payout), decimal(0)).toDecimalPlaces(6);
}

function totalRoundStake(metadata: BlackjackMetadata) {
  return totalHandStake(metadata).plus(totalSideBetStake(metadata.sideBets ?? [])).toDecimalPlaces(6);
}

function assertBlackjackOwnerAndActive(round: BlackjackRoundShape, userId: string) {
  if (round.userId !== userId || round.game !== CasinoGameKey.BLACKJACK) {
    throw new ConflictError("Partie Blackjack introuvable.", "BLACKJACK_ROUND_NOT_FOUND");
  }
  if (round.status !== CasinoRoundStatus.ACTIVE) {
    throw new ConflictError("Cette partie Blackjack est terminée.", "BLACKJACK_ROUND_NOT_ACTIVE");
  }
}

function normalizeActiveHandIndex(hands: BlackjackHand[], activeHandIndex: number) {
  if (activeHandIndex >= 0 && hands[activeHandIndex]?.status === "PLAYING") {
    return activeHandIndex;
  }
  return hands.findIndex((hand) => hand.status === "PLAYING");
}

function parseBlackjackMetadata(value: Prisma.JsonValue | null): BlackjackMetadata {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new ConflictError("Métadonnées Blackjack invalides.", "BLACKJACK_INVALID_METADATA");
  }
  const metadata = value as Partial<BlackjackMetadata>;
  if (!Array.isArray(metadata.deck) || !Array.isArray(metadata.dealerCards) || !Array.isArray(metadata.hands)) {
    throw new ConflictError("Métadonnées Blackjack invalides.", "BLACKJACK_INVALID_METADATA");
  }
  const hands = metadata.hands;
  const activeHandIndex = typeof metadata.activeHandIndex === "number" ? metadata.activeHandIndex : -1;

  return {
    deck: metadata.deck,
    dealerCards: metadata.dealerCards,
    hands,
    activeHandIndex: normalizeActiveHandIndex(hands, activeHandIndex),
    settledAt: metadata.settledAt,
    dealerResult: metadata.dealerResult,
    sideBets: Array.isArray(metadata.sideBets) ? metadata.sideBets : [],
    stakeSplit: metadata.stakeSplit,
    ledgerEntryIds: metadata.ledgerEntryIds
  };
}

function serializeBlackjackRound(round: BlackjackRoundShape) {
  const metadata = parseBlackjackMetadata(round.metadata);
  const publicRound = serializeRound(round);
  const currentHand = metadata.hands[metadata.activeHandIndex];
  const dealerVisibleCards: PublicBlackjackCard[] =
    round.status === CasinoRoundStatus.ACTIVE && metadata.dealerCards.length > 1
      ? [metadata.dealerCards[0], { ...metadata.dealerCards[1], hidden: true }]
      : metadata.dealerCards;

  return {
    ...publicRound,
    dealerCards: dealerVisibleCards,
    dealerTotal: blackjackValue(dealerVisibleCards.filter((card) => !card.hidden)).total,
    dealerFinalTotal: round.status === CasinoRoundStatus.ACTIVE ? null : blackjackValue(metadata.dealerCards).total,
    dealerResult: metadata.dealerResult,
    hands: metadata.hands.map((hand, index) => ({
      ...hand,
      total: blackjackValue(hand.cards).total,
      soft: blackjackValue(hand.cards).soft,
      active: index === metadata.activeHandIndex
    })),
    sideBets: metadata.sideBets ?? [],
    activeHandIndex: metadata.activeHandIndex,
    availableActions:
      round.status === CasinoRoundStatus.ACTIVE && currentHand
        ? {
            hit: currentHand.status === "PLAYING",
            stand: currentHand.status === "PLAYING",
            double: canDouble(currentHand),
            split: metadata.hands.length === 1 && canSplit(currentHand)
          }
        : { hit: false, stand: false, double: false, split: false }
  };
}

function toJson(value: BlackjackMetadata): Prisma.InputJsonValue {
  return JSON.parse(JSON.stringify(value));
}
