import { BetTicketStatus, BoostedOddStatus, EventStatus, LedgerEntryType, MarketStatus, Prisma, SelectionStatus } from "@prisma/client";
import { z } from "zod";
import { prisma } from "@/server/db";
import { ConflictError } from "@/server/errors";
import { scoreStakeRisk } from "@/server/risk/rules";
import { recordWagerInTx } from "@/server/vip/rewards";
import { debitPlayableBalanceInTx, decimal } from "@/server/wallet/ledger";
import { writeAuditLog } from "@/server/audit";

export const betPlacementSchema = z.object({
  currency: z.string().min(3).max(8),
  stake: z.coerce.number().positive().max(100000),
  acceptOddsChange: z.enum(["none", "higher", "any"]).default("none"),
  sameGame: z.boolean().default(false),
  selections: z
    .array(
      z.object({
        selectionId: z.string().uuid(),
        odds: z.coerce.number().positive(),
        oddsVersion: z.number().int().positive()
      })
    )
    .min(1)
    .max(20)
});

export type BetPlacementInput = z.infer<typeof betPlacementSchema>;

export function assertNoCorrelatedSelections(
  selections: Array<{ id: string; marketId: string; market: { eventId: string; name?: string } }>,
  options: { sameGame?: boolean } = {}
) {
  if (selections.length <= 1) {
    return;
  }

  const events = new Map<string, string>();
  const markets = new Map<string, string>();
  const sameGameEventId = selections[0]?.market.eventId;

  for (const selection of selections) {
    if (options.sameGame && selection.market.eventId !== sameGameEventId) {
      throw new ConflictError("Un combiné sur le même match doit contenir uniquement des sélections du même événement.", "SAME_GAME_EVENT_MISMATCH", {
        eventId: selection.market.eventId,
        expectedEventId: sameGameEventId
      });
    }

    const previousEventSelectionId = events.get(selection.market.eventId);
    if (previousEventSelectionId && !options.sameGame) {
      throw new ConflictError("Les sélections du même événement ne peuvent pas être combinées.", "CORRELATED_SELECTIONS", {
        eventId: selection.market.eventId,
        selectionIds: [previousEventSelectionId, selection.id]
      });
    }

    const previousMarketSelectionId = markets.get(selection.marketId);
    if (previousMarketSelectionId) {
      throw new ConflictError("Les sélections du même marché ne peuvent pas être combinées.", "CORRELATED_SELECTIONS", {
        marketId: selection.marketId,
        selectionIds: [previousMarketSelectionId, selection.id]
      });
    }

    events.set(selection.market.eventId, selection.id);
    markets.set(selection.marketId, selection.id);
  }

  if (options.sameGame && selections.length < 2) {
    throw new ConflictError("Un combiné sur le même match nécessite au moins deux sélections.", "SAME_GAME_MIN_SELECTIONS");
  }
}

export function assertBoostedCumulativeStakeLimit(input: {
  selectionId: string;
  boostedOddId: string;
  existingStake: string | number | Prisma.Decimal;
  requestedStake: string | number | Prisma.Decimal;
  maxStake: string | number | Prisma.Decimal;
}) {
  const existingStake = decimal(input.existingStake);
  const requestedStake = decimal(input.requestedStake);
  const maxStake = decimal(input.maxStake);
  const totalStake = existingStake.plus(requestedStake);

  if (totalStake.gt(maxStake)) {
    throw new ConflictError("La mise cumulée dépasse le maximum autorisé pour cette cote boostée.", "BOOSTED_ODD_MAX_STAKE_TOTAL", {
      selectionId: input.selectionId,
      boostedOddId: input.boostedOddId,
      existingStake: existingStake.toString(),
      requestedStake: requestedStake.toString(),
      totalStake: totalStake.toString(),
      maxStake: maxStake.toString()
    });
  }
}

export async function placeBet(userId: string, input: BetPlacementInput) {
  const stake = decimal(input.stake);

  return prisma.$transaction(async (tx) => {
    const selections = await tx.selection.findMany({
      where: { id: { in: input.selections.map((selection) => selection.selectionId) } },
      include: {
        market: {
          include: {
            event: {
              include: {
                competition: true,
                sport: true
              }
            }
          }
        },
        boostedOdd: true
      }
    });

    if (selections.length !== input.selections.length) {
      throw new ConflictError("One or more selections are no longer available", "SELECTION_NOT_FOUND");
    }

    assertNoCorrelatedSelections(selections, { sameGame: input.sameGame });

    const requestedById = new Map(input.selections.map((selection) => [selection.selectionId, selection]));

    let totalOdds = decimal(1);
    for (const selection of selections) {
      const requested = requestedById.get(selection.id)!;
      const event = selection.market.event;

      if (event.status !== EventStatus.SCHEDULED || event.isLive || event.startsAt <= new Date()) {
        throw new ConflictError("Ce match n'est plus ouvert aux paris pré-match.", "EVENT_NOT_OPEN", {
          eventId: event.id,
          startsAt: event.startsAt.toISOString(),
          liveDisabled: event.isLive
        });
      }
      if (selection.market.status !== MarketStatus.OPEN || selection.status !== SelectionStatus.ACTIVE) {
        throw new ConflictError("Market or selection is suspended", "MARKET_SUSPENDED", {
          marketId: selection.marketId,
          selectionId: selection.id
        });
      }

      const providerOdds = decimal(selection.odds);
      const requestedOdds = decimal(requested.odds);
      const oddsChanged = selection.oddsVersion !== requested.oddsVersion || !providerOdds.equals(requestedOdds);
      const lowerOdds = providerOdds.lt(requestedOdds);

      if (oddsChanged && (input.acceptOddsChange === "none" || (lowerOdds && input.acceptOddsChange !== "any"))) {
        throw new ConflictError("Odds changed before placement", "ODDS_CHANGED", {
          selectionId: selection.id,
          requestedOdds: requestedOdds.toString(),
          currentOdds: providerOdds.toString(),
          currentOddsVersion: selection.oddsVersion
        });
      }

      if (selection.boostedOdd) {
        if (input.sameGame) {
          throw new ConflictError("Les cotes boostées ne sont pas disponibles dans le combiné sur le même match.", "BOOSTED_ODD_SAME_GAME_BLOCKED", {
            selectionId: selection.id,
            boostedOddId: selection.boostedOdd.id
          });
        }

        if (selection.boostedOdd.status !== BoostedOddStatus.ACTIVE) {
          throw new ConflictError("Cette cote boostée n'est plus disponible.", "BOOSTED_ODD_NOT_ACTIVE", {
            selectionId: selection.id,
            boostedOddId: selection.boostedOdd.id
          });
        }

        if (input.selections.length > 1 && !selection.boostedOdd.allowCombinations) {
          throw new ConflictError("Cette cote boostée est disponible en pari simple uniquement.", "BOOSTED_ODD_NOT_COMBINABLE", {
            selectionId: selection.id,
            boostedOddId: selection.boostedOdd.id
          });
        }

        const existingBoostedStake = await tx.betTicket.aggregate({
          where: {
            userId,
            status: { notIn: [BetTicketStatus.VOID, BetTicketStatus.CANCELLED] },
            selections: { some: { selectionId: selection.id } }
          },
          _sum: { stake: true }
        });

        assertBoostedCumulativeStakeLimit({
          selectionId: selection.id,
          boostedOddId: selection.boostedOdd.id,
          existingStake: existingBoostedStake._sum.stake ?? 0,
          requestedStake: stake,
          maxStake: selection.boostedOdd.maxStake
        });

      }

      totalOdds = totalOdds.mul(providerOdds);
    }

    const potentialPayout = stake.mul(totalOdds).toDecimalPlaces(6);
    const risk = scoreStakeRisk({ stake, potentialPayout });

    const { wallet, entries, availableDebit, bonusDebit } = await debitPlayableBalanceInTx(tx, {
      userId,
      currency: input.currency,
      amount: stake,
      type: LedgerEntryType.BET_STAKE,
      referenceType: "bet_ticket",
      reason: "Sportsbook stake"
    });

    const ticket = await tx.betTicket.create({
      data: {
        userId,
        walletId: wallet.id,
        stake,
        totalOdds,
        potentialPayout,
        currency: input.currency,
        status: BetTicketStatus.OPEN,
        riskScore: risk.score,
        metadata: {
          sameGame: input.sameGame,
          stakeSplit: {
            available: availableDebit.toString(),
            bonus: bonusDebit.toString()
          }
        },
        selections: {
          create: selections.map((selection) => ({
            eventId: selection.market.eventId,
            marketId: selection.marketId,
            selectionId: selection.id,
            eventName: `${selection.market.event.homeName} vs ${selection.market.event.awayName}`,
            marketName: selection.market.name,
            selectionName: selection.name,
            odds: selection.odds,
            oddsVersion: selection.oddsVersion,
            snapshot: {
              sport: selection.market.event.sport.name,
              competition: selection.market.event.competition.name,
              startsAt: selection.market.event.startsAt.toISOString(),
              isLive: selection.market.event.isLive,
              boostedOdd: selection.boostedOdd
                ? {
                    id: selection.boostedOdd.id,
                    title: selection.boostedOdd.title,
                    originalOdds: selection.boostedOdd.originalOdds.toString(),
                    maxStake: selection.boostedOdd.maxStake.toString(),
                    allowCombinations: selection.boostedOdd.allowCombinations,
                    terms: selection.boostedOdd.terms
                  }
                : undefined
            }
          }))
        }
      },
      include: { selections: true }
    });

    await tx.ledgerEntry.updateMany({
      where: { id: { in: entries.map((entry) => entry.id) } },
      data: { referenceId: ticket.id }
    });

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

    await writeAuditLog(
      {
        actorUserId: userId,
        action: "sportsbook.bet.place",
        entityType: "BetTicket",
        entityId: ticket.id,
        after: {
          stake: stake.toString(),
          stakeSplit: {
            available: availableDebit.toString(),
            bonus: bonusDebit.toString()
          },
          totalOdds: totalOdds.toString(),
          potentialPayout: potentialPayout.toString(),
          riskScore: risk.score
        }
      },
      tx
    );

    if (risk.score >= 25) {
      await tx.riskEvent.create({
        data: {
          userId,
          category: "bet_placement",
          score: risk.score,
          severity: risk.severity,
          reason: "Stake or payout threshold reached",
          metadata: {
            ticketId: ticket.id,
            stake: stake.toString(),
            potentialPayout: potentialPayout.toString()
          }
        }
      });
    }

    return ticket;
  }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
}
