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 { debitPlayableBalanceInTx, decimal } from "@/server/wallet/ledger";

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

export const diceConfig = {
  currency: "EUR",
  minStake: "0.01",
  maxStake: "10000",
  maxPayout: "100000",
  minChance: 1,
  maxChance: 98,
  houseEdgeBps: 100
};

export const dicePlaySchema = z.object({
  currency: z.string().min(3).max(8).default("EUR"),
  stake: z.coerce.number().positive().max(Number(diceConfig.maxStake)),
  chance: z.coerce.number().min(diceConfig.minChance).max(diceConfig.maxChance),
  direction: z.enum(["over", "under"]).default("over"),
  clientRequestId: z.string().uuid().optional()
});

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

export const diceVerifySchema = 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 DicePlayInput = z.infer<typeof dicePlaySchema>;

export function sha256(value: string) {
  return crypto.createHash("sha256").update(value).digest("hex");
}

export function createServerSeed() {
  return crypto.randomBytes(32).toString("hex");
}

export function createClientSeed() {
  return `zira-${crypto.randomBytes(8).toString("hex")}`;
}

export function calculateDiceNumbers(input: { chance: string | number | Prisma.Decimal; direction: "over" | "under" }) {
  const chance = decimal(input.chance).toDecimalPlaces(4);
  if (chance.lt(diceConfig.minChance) || chance.gt(diceConfig.maxChance)) {
    throw new ConflictError("La chance doit être comprise entre 1% et 98%.", "DICE_INVALID_CHANCE");
  }

  const multiplier = decimal(100).minus(HOUSE_EDGE_PERCENT).div(chance).toDecimalPlaces(6, Prisma.Decimal.ROUND_DOWN);
  const target = input.direction === "over" ? decimal(100).minus(chance) : chance;

  return {
    chance,
    multiplier,
    target: target.toDecimalPlaces(4),
    direction: input.direction
  };
}

export function calculateDiceResult(input: { serverSeed: string; clientSeed: string; nonce: number }) {
  const digest = crypto
    .createHmac("sha256", input.serverSeed)
    .update(`${input.clientSeed}:${input.nonce}:dice`)
    .digest("hex");
  const sample = Number(BigInt(`0x${digest.slice(0, 13)}`));
  const roll = (sample / TWO_POWER_52) * 100;

  return Math.floor(roll * 100) / 100;
}

export function isDiceWin(input: { result: string | number | Prisma.Decimal; target: string | number | Prisma.Decimal; direction: "over" | "under" }) {
  const result = decimal(input.result);
  const target = decimal(input.target);
  return input.direction === "over" ? result.gt(target) : result.lt(target);
}

export async function getDiceState(userId: string, currency: string) {
  const seedPair = await ensureDiceSeedPair(userId);
  const rounds = await prisma.casinoRound.findMany({
    where: { userId, game: CasinoGameKey.DICE },
    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: diceConfig,
    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 playDice(userId: string, input: DicePlayInput) {
  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à.", "DICE_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(diceConfig.minStake)) {
      throw new ConflictError(`La mise minimum est de ${diceConfig.minStake} €.`, "DICE_MIN_STAKE");
    }

    const dice = calculateDiceNumbers({ chance: input.chance, direction: input.direction });
    const potentialPayout = stake.mul(dice.multiplier).toDecimalPlaces(6);
    if (potentialPayout.gt(diceConfig.maxPayout)) {
      throw new ConflictError("Le gain potentiel dépasse la limite autorisée.", "DICE_MAX_PAYOUT", {
        maxPayout: diceConfig.maxPayout,
        potentialPayout: potentialPayout.toString()
      });
    }

    const seedPair = await ensureDiceSeedPairInTx(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(calculateDiceResult({ serverSeed: lockedSeedPair.serverSeed, clientSeed: lockedSeedPair.clientSeed, nonce })).toDecimalPlaces(2);
    const won = isDiceWin({ result, target: dice.target, direction: dice.direction });
    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:dice:${input.clientRequestId}:stake` : undefined,
      reason: "Dice stake",
      metadata: {
        game: "dice",
        chance: dice.chance.toString(),
        direction: dice.direction
      }
    });

    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:dice:${input.clientRequestId}:win` : undefined,
          reason: "Dice win",
          metadata: {
            game: "dice",
            result: result.toString(),
            multiplier: dice.multiplier.toString()
          }
        }))
      );
    }

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

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

export async function rotateDiceSeed(userId: string, input: z.infer<typeof diceRotateSeedSchema>) {
  return prisma.$transaction(async (tx) => {
    const current = await ensureDiceSeedPairInTx(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.DICE,
        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 verifyDice(input: z.infer<typeof diceVerifySchema>) {
  return {
    serverSeedHash: sha256(input.serverSeed),
    result: calculateDiceResult(input).toFixed(2)
  };
}

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

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

  if (existing) {
    return existing;
  }

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

export function serializeRound(round: {
  id: string;
  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;
}) {
  return {
    id: round.id,
    currency: round.currency,
    stake: round.stake.toString(),
    payout: round.payout.toString(),
    profit: round.profit.toString(),
    multiplier: round.multiplier.toString(),
    chance: round.chance.toString(),
    target: round.target.toString(),
    direction: round.direction,
    result: round.result.toString(),
    nonce: round.nonce,
    status: round.status,
    serverSeedHash: round.serverSeedHash,
    createdAt: round.createdAt.toISOString()
  };
}
