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

const HOUSE_EDGE_PERCENT = decimal(1);
const TWO_POWER_52 = Number(0x10000000000000n);

export const limboConfig = {
  currency: "EUR",
  minStake: "0.01",
  maxStake: "10000",
  maxPayout: "100000",
  minMultiplier: 1.01,
  maxMultiplier: 1000000,
  houseEdgeBps: 100
};

export const limboPlaySchema = z.object({
  currency: z.string().min(3).max(8).default("EUR"),
  stake: z.coerce.number().positive().max(Number(limboConfig.maxStake)),
  multiplier: z.coerce.number().min(limboConfig.minMultiplier).max(limboConfig.maxMultiplier),
  clientRequestId: z.string().uuid().optional()
});

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

export const limboVerifySchema = 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 LimboPlayInput = z.infer<typeof limboPlaySchema>;

export function calculateLimboNumbers(input: { multiplier: string | number | Prisma.Decimal }) {
  const multiplier = decimal(input.multiplier).toDecimalPlaces(2, Prisma.Decimal.ROUND_DOWN);
  if (multiplier.lt(limboConfig.minMultiplier) || multiplier.gt(limboConfig.maxMultiplier)) {
    throw new ConflictError(
      `Le multiplicateur doit être compris entre ${limboConfig.minMultiplier}x et ${limboConfig.maxMultiplier}x.`,
      "LIMBO_INVALID_MULTIPLIER"
    );
  }

  const chance = decimal(100)
    .minus(HOUSE_EDGE_PERCENT)
    .div(multiplier)
    .toDecimalPlaces(8, Prisma.Decimal.ROUND_DOWN);

  return {
    multiplier,
    target: multiplier,
    chance
  };
}

export function calculateLimboResult(input: { serverSeed: string; clientSeed: string; nonce: number }) {
  const digest = crypto
    .createHmac("sha256", input.serverSeed)
    .update(`${input.clientSeed}:${input.nonce}:limbo`)
    .digest("hex");
  const sample = Number(BigInt(`0x${digest.slice(0, 13)}`));
  const uniform = (sample + 1) / (TWO_POWER_52 + 1);
  const raw = decimal(100)
    .minus(HOUSE_EDGE_PERCENT)
    .div(decimal(1).minus(uniform));
  const multiplier = raw.div(100).toDecimalPlaces(2, Prisma.Decimal.ROUND_DOWN);

  if (multiplier.gt(limboConfig.maxMultiplier)) {
    return Number(limboConfig.maxMultiplier);
  }

  return Math.max(1, multiplier.toNumber());
}

export function isLimboWin(input: { result: string | number | Prisma.Decimal; target: string | number | Prisma.Decimal }) {
  return decimal(input.result).gte(decimal(input.target));
}

export async function getLimboState(userId: string, currency: string) {
  const seedPair = await ensureLimboSeedPair(userId);
  const rounds = await prisma.casinoRound.findMany({
    where: { userId, game: CasinoGameKey.LIMBO },
    orderBy: { createdAt: "desc" },
    take: 25
  });

  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: limboConfig,
    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
    },
    recentRounds: rounds.map(serializeRound)
  };
}

export async function playLimbo(userId: string, input: LimboPlayInput) {
  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à.", "LIMBO_REQUEST_ID_CONFLICT");
      }
      return serializeRound(existing);
    }
  }

  return prisma.$transaction(async (tx) => {
    const roundId = crypto.randomUUID();
    const stake = decimal(input.stake).toDecimalPlaces(6);
    if (stake.lt(limboConfig.minStake)) {
      throw new ConflictError(`La mise minimum est de ${limboConfig.minStake} €.`, "LIMBO_MIN_STAKE");
    }

    const limbo = calculateLimboNumbers({ multiplier: input.multiplier });
    const potentialPayout = stake.mul(limbo.multiplier).toDecimalPlaces(6);
    if (potentialPayout.gt(limboConfig.maxPayout)) {
      throw new ConflictError("Le gain potentiel dépasse la limite autorisée.", "LIMBO_MAX_PAYOUT", {
        maxPayout: limboConfig.maxPayout,
        potentialPayout: potentialPayout.toString()
      });
    }

    const seedPair = await ensureLimboSeedPairInTx(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 result = decimal(calculateLimboResult({ serverSeed: lockedSeedPair.serverSeed, clientSeed: lockedSeedPair.clientSeed, nonce })).toDecimalPlaces(2);
    const won = isLimboWin({ result, target: limbo.target });
    const payout = won ? potentialPayout : decimal(0);
    const profit = won ? payout.minus(stake).toDecimalPlaces(6) : stake.negated().toDecimalPlaces(6);

    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:limbo:${input.clientRequestId}:stake` : undefined,
      reason: "Limbo stake",
      metadata: {
        game: "limbo",
        target: limbo.target.toString(),
        chance: limbo.chance.toString()
      }
    });

    if (won && payout.gt(0)) {
      entries.push(
        ...(await creditPayoutByStakeSourceInTx(tx, {
          userId,
          walletId: wallet.id,
          currency: input.currency,
          stake,
          payout,
          availableDebit,
          bonusDebit,
          type: LedgerEntryType.CASINO_WIN,
          referenceType: "casino_round",
          referenceId: roundId,
          idempotencyKey: input.clientRequestId ? `casino:limbo:${input.clientRequestId}:win` : undefined,
          reason: "Limbo win",
          metadata: {
            game: "limbo",
            result: result.toString(),
            multiplier: limbo.multiplier.toString()
          }
        }))
      );
    }

    const round = await tx.casinoRound.create({
      data: {
        id: roundId,
        userId,
        walletId: wallet.id,
        seedPairId: lockedSeedPair.id,
        game: CasinoGameKey.LIMBO,
        clientRequestId: input.clientRequestId,
        currency: input.currency,
        stake,
        payout,
        profit,
        multiplier: limbo.multiplier,
        chance: limbo.chance,
        target: limbo.target,
        direction: "target",
        result,
        nonce,
        status: won ? CasinoRoundStatus.WON : CasinoRoundStatus.LOST,
        serverSeedHash: lockedSeedPair.serverSeedHash,
        metadata: {
          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.limbo.play",
        entityType: "CasinoRound",
        entityId: round.id,
        after: {
          stake: stake.toString(),
          payout: payout.toString(),
          result: result.toString(),
          target: limbo.target.toString(),
          status: round.status,
          stakeSplit: {
            available: availableDebit.toString(),
            bonus: bonusDebit.toString()
          }
        }
      },
      tx
    );

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

export async function rotateLimboSeed(userId: string, input: z.infer<typeof limboRotateSeedSchema>) {
  return prisma.$transaction(async (tx) => {
    const current = await ensureLimboSeedPairInTx(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.LIMBO,
        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 verifyLimbo(input: z.infer<typeof limboVerifySchema>) {
  return {
    serverSeedHash: sha256(input.serverSeed),
    result: calculateLimboResult(input).toFixed(2)
  };
}

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

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

  if (existing) {
    return existing;
  }

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