import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { FundsPostingService } from '../ledger/funds-posting.service';
import { BettingLimitsService } from './betting-limits.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBetNo } from '../../shared/common/decorators';
import { appBadRequest, appNotFound } from '../../shared/common/app-error';
import {
  PARLAY_MIN_LEGS,
  PARLAY_MAX_LEGS,
  canSelectForParlay,
  defaultMarketName,
  defaultSelectionName,
  isPreMatchKickoff,
  isSupportedSport,
  resolveMarketText,
  resolveTranslationFallback,
} from '@thebet365/shared';

type TxClient = Prisma.TransactionClient;
type PrismaClientLike = PrismaService | TxClient;

type MatchContext = {
  matchLabel: string;
  leagueName: string;
};

type BetSelectionRow = {
  matchId: bigint | null;
  marketType: string;
  period: string | null;
  marketNameSnapshot: string | null;
  selectionNameSnapshot: string;
  handicapLine: Decimal | null;
  totalLine: Decimal | null;
  odds: Decimal;
  sortOrder?: number;
};

type SelectionWithMarketLabel = {
  selectionCode: string;
  selectionName: string;
  nameI18n: unknown;
  market: {
    marketType: string;
    nameI18n: unknown;
  };
};

interface BetSelectionInput {
  selectionId: bigint;
  oddsVersion: bigint;
  stake?: number;
}

@Injectable()
export class BetsService {
  constructor(
    private prisma: PrismaService,
    private funds: FundsPostingService,
    private bettingLimits: BettingLimitsService,
  ) {}

  private async validateSelection(
    selectionId: bigint,
    oddsVersion: bigint,
    options?: { forParlay?: boolean; tx?: TxClient },
  ) {
    const client: PrismaClientLike = options?.tx ?? this.prisma;
    const selection = await client.marketSelection.findUnique({
      where: { id: selectionId },
      include: { market: { include: { match: true } } },
    });

    if (!selection) throw appBadRequest('SELECTION_NOT_FOUND');
    if (selection.status !== 'OPEN') throw appBadRequest('SELECTION_CLOSED');
    if (selection.market.status !== 'OPEN') throw appBadRequest('MARKET_CLOSED');
    if (selection.market.showOnPlayer === false) throw appBadRequest('MARKET_CLOSED');
    if (!options?.forParlay && selection.market.allowSingle === false) {
      throw appBadRequest('SINGLE_MARKET_NOT_ALLOWED');
    }
    if (selection.market.match.status !== 'PUBLISHED') {
      throw appBadRequest('MATCH_NOT_BETTING');
    }
    if (!isSupportedSport(selection.market.match.sportType)) {
      throw appBadRequest('FOOTBALL_ONLY');
    }
    if (!selection.market.match.isOutright && !isPreMatchKickoff(selection.market.match.startTime)) {
      throw appBadRequest('PRE_MATCH_ONLY');
    }
    if (selection.oddsVersion !== oddsVersion) {
      throw appBadRequest('ODDS_CHANGED');
    }

    if (options?.forParlay) {
      const line = selection.market.lineValue ? Number(selection.market.lineValue) : null;
      const check = canSelectForParlay({
        marketType: selection.market.marketType,
        lineValue: line,
        allowParlay: selection.market.allowParlay,
        isOutright: selection.market.match.isOutright,
      });
      if (!check.ok) {
        const code =
          check.reason === 'OUTRIGHT'
            ? 'PARLAY_OUTRIGHT_FORBIDDEN'
            : check.reason === 'QUARTER_LINE'
              ? 'PARLAY_QUARTER_LINE_FORBIDDEN'
              : 'PARLAY_MARKET_NOT_ALLOWED';
        throw appBadRequest(code);
      }
    }

    return selection;
  }

  async placeSingleBet(
    userId: bigint,
    agentId: bigint | null,
    selectionId: bigint,
    oddsVersion: bigint,
    stake: number,
    requestId: string,
  ) {
    if (stake <= 0) throw appBadRequest('INVALID_STAKE');

    const stakeDec = new Decimal(stake);

    try {
      return await this.prisma.$transaction(async (tx) => {
        const existing = await tx.bet.findUnique({
          where: { userId_requestId: { userId, requestId } },
        });
        if (existing) return existing;

        const selection = await this.validateSelection(selectionId, oddsVersion, {
          forParlay: false,
          tx,
        });
        const locale = await this.resolveUserLocale(userId, tx);
        const odds = new Decimal(selection.odds.toString());
        const potentialReturn = stakeDec.mul(odds);
        await this.bettingLimits.validateBet({
          userId,
          betType: 'SINGLE',
          stake,
          potentialReturn,
          tx,
        });
        const betNo = generateBetNo();

        const created = await tx.bet.create({
          data: {
            betNo,
            userId,
            agentId,
            betType: 'SINGLE',
            stake: stakeDec,
            totalOdds: odds,
            potentialReturn,
            requestId,
            selections: {
              create: {
                matchId: selection.market.matchId,
                marketId: selection.marketId,
                selectionId: selection.id,
                marketType: selection.market.marketType,
                period: selection.market.period,
                marketNameSnapshot: this.marketNameSnapshot(selection, locale),
                selectionNameSnapshot: this.selectionNameSnapshot(selection, locale),
                handicapLine: selection.market.lineValue,
                totalLine: selection.market.lineValue,
                odds,
                oddsVersion,
              },
            },
          },
          include: { selections: true },
        });

        await this.funds.freezeBet({ userId, stake: stakeDec, betNo, tx });
        return created;
      });
    } catch (error) {
      const existing = await this.findExistingRequestBetOnUniqueError(userId, requestId, error);
      if (existing) return existing;
      throw error;
    }
  }

  async placeParlayBet(
    userId: bigint,
    agentId: bigint | null,
    legs: BetSelectionInput[],
    stake: number,
    requestId: string,
  ) {
    if (stake <= 0) throw appBadRequest('INVALID_STAKE');
    if (legs.length < PARLAY_MIN_LEGS || legs.length > PARLAY_MAX_LEGS) {
      throw appBadRequest('PARLAY_LEG_COUNT_INVALID', { min: PARLAY_MIN_LEGS, max: PARLAY_MAX_LEGS });
    }

    const stakeDec = new Decimal(stake);

    try {
      return await this.prisma.$transaction(async (tx) => {
        const existing = await tx.bet.findUnique({
          where: { userId_requestId: { userId, requestId } },
        });
        if (existing) return existing;

        const selections: Awaited<ReturnType<typeof this.validateSelection>>[] = [];
        for (const leg of legs) {
          const sel = await this.validateSelection(leg.selectionId, leg.oddsVersion, {
            forParlay: true,
            tx,
          });
          selections.push(sel);
        }
        const matchIds = new Set<string>();
        for (const sel of selections) {
          const matchKey = sel.market.matchId.toString();
          if (matchIds.has(matchKey)) {
            throw appBadRequest('PARLAY_SAME_MATCH_FORBIDDEN');
          }
          matchIds.add(matchKey);
        }
        const locale = await this.resolveUserLocale(userId, tx);

        let totalOdds = new Decimal(1);
        for (const sel of selections) {
          totalOdds = totalOdds.mul(sel.odds.toString());
        }

        const potentialReturn = stakeDec.mul(totalOdds);
        await this.bettingLimits.validateBet({
          userId,
          betType: 'PARLAY',
          stake,
          potentialReturn,
          tx,
        });
        const betNo = generateBetNo();

        const created = await tx.bet.create({
          data: {
            betNo,
            userId,
            agentId,
            betType: 'PARLAY',
            stake: stakeDec,
            totalOdds,
            potentialReturn,
            requestId,
            selections: {
              create: selections.map((sel, i) => ({
                matchId: sel.market.matchId,
                marketId: sel.marketId,
                selectionId: sel.id,
                marketType: sel.market.marketType,
                period: sel.market.period,
                marketNameSnapshot: this.marketNameSnapshot(sel, locale),
                selectionNameSnapshot: this.selectionNameSnapshot(sel, locale),
                handicapLine: sel.market.lineValue,
                totalLine: sel.market.lineValue,
                odds: sel.odds,
                oddsVersion: legs[i].oddsVersion,
                sortOrder: i,
              })),
            },
          },
          include: { selections: true },
        });

        await this.funds.freezeBet({ userId, stake: stakeDec, betNo, tx });
        return created;
      });
    } catch (error) {
      const existing = await this.findExistingRequestBetOnUniqueError(userId, requestId, error);
      if (existing) return existing;
      throw error;
    }
  }

  private async findExistingRequestBetOnUniqueError(
    userId: bigint,
    requestId: string,
    error: unknown,
  ) {
    if (
      error instanceof Prisma.PrismaClientKnownRequestError &&
      error.code === 'P2002'
    ) {
      return this.prisma.bet.findUnique({ where: { userId_requestId: { userId, requestId } } });
    }
    return null;
  }

  async getUserBets(
    userId: bigint,
    status?: string,
    page = 1,
    pageSize = 20,
    matchId?: bigint,
  ) {
    const where = {
      userId,
      ...(status ? { status } : {}),
      ...(matchId ? { selections: { some: { matchId } } } : {}),
    };
    const skip = (page - 1) * pageSize;
    const [items, total] = await Promise.all([
      this.prisma.bet.findMany({
        where,
        include: { selections: true },
        orderBy: { placedAt: 'desc' },
        skip,
        take: pageSize,
      }),
      this.prisma.bet.count({ where }),
    ]);
    return { items, total, page, pageSize };
  }

  async getUserBetStats(userId: bigint) {
    const [byStatus, aggregates] = await Promise.all([
      this.prisma.bet.groupBy({
        by: ['status'],
        where: { userId },
        _count: { id: true },
        _sum: { stake: true, actualReturn: true, potentialReturn: true },
      }),
      this.prisma.bet.aggregate({
        where: { userId },
        _sum: { stake: true, actualReturn: true, potentialReturn: true },
        _count: { id: true },
      }),
    ]);

    return {
      total: aggregates._count.id,
      totalStake: aggregates._sum.stake?.toString() ?? '0',
      totalReturn: aggregates._sum.actualReturn?.toString() ?? '0',
      totalPotentialReturn: aggregates._sum.potentialReturn?.toString() ?? '0',
      byStatus: byStatus.map((g) => ({
        status: g.status,
        count: g._count.id,
        totalStake: g._sum.stake?.toString() ?? '0',
        totalReturn: g._sum.actualReturn?.toString() ?? '0',
      })),
    };
  }

  async getBetByNo(betNo: string, userId?: bigint) {
    return this.prisma.bet.findFirst({
      where: { betNo, ...(userId ? { userId } : {}) },
      include: { selections: true },
    });
  }

  private dec(v: Decimal | null | undefined) {
    return v?.toString() ?? '0';
  }

  private resolveEntityName(
    translations: Array<{ entityType: string; entityId: bigint; locale: string; value: string }>,
    entityType: string,
    entityId: bigint,
    locale: string,
  ) {
    const map = Object.fromEntries(
      translations
        .filter(
          (t) =>
            t.entityType === entityType && t.entityId.toString() === entityId.toString(),
        )
        .map((t) => [t.locale, t.value]),
    );
    return resolveTranslationFallback(map, locale);
  }

  private async loadMatchContext(
    matchIds: bigint[],
    locale = 'zh-CN',
  ): Promise<Map<string, MatchContext>> {
    const result = new Map<string, MatchContext>();
    if (matchIds.length === 0) return result;

    const matches = await this.prisma.match.findMany({
      where: { id: { in: matchIds } },
      select: {
        id: true,
        matchName: true,
        leagueId: true,
        homeTeamId: true,
        awayTeamId: true,
        homeTeam: { select: { code: true } },
        awayTeam: { select: { code: true } },
      },
    });

    const entityIds = new Set<bigint>();
    for (const m of matches) {
      entityIds.add(m.leagueId);
      entityIds.add(m.homeTeamId);
      entityIds.add(m.awayTeamId);
    }

    const translations =
      entityIds.size > 0
        ? await this.prisma.entityTranslation.findMany({
            where: {
              entityId: { in: Array.from(entityIds) },
              entityType: { in: ['TEAM', 'LEAGUE'] },
              fieldName: 'name',
            },
          })
        : [];

    for (const m of matches) {
      const leagueName =
        this.resolveEntityName(translations, 'LEAGUE', m.leagueId, locale) || m.leagueId.toString();
      const homeName =
        this.resolveEntityName(translations, 'TEAM', m.homeTeamId, locale) || m.homeTeam.code;
      const awayName =
        this.resolveEntityName(translations, 'TEAM', m.awayTeamId, locale) || m.awayTeam.code;
      const matchLabel = m.matchName?.trim() || `${homeName} vs ${awayName}`;
      result.set(m.id.toString(), { matchLabel, leagueName });
    }

    return result;
  }

  private formatSelectionPreviews(
    selections: BetSelectionRow[],
    matchContext: Map<string, MatchContext>,
  ) {
    return selections.map((s) => {
      const ctx = s.matchId ? matchContext.get(s.matchId.toString()) : undefined;
      return {
        matchId: s.matchId?.toString() ?? null,
        matchLabel: ctx?.matchLabel ?? '—',
        leagueName: ctx?.leagueName ?? '',
        marketType: s.marketType,
        marketName: s.marketNameSnapshot ?? null,
        period: s.period,
        selectionName: s.selectionNameSnapshot,
        odds: this.dec(s.odds),
      };
    });
  }

  private attachSelectionPreviews<T extends Record<string, unknown>>(
    row: T,
    selections: BetSelectionRow[],
    matchContext: Map<string, MatchContext>,
  ) {
    const selectionPreviews = this.formatSelectionPreviews(selections, matchContext);
    const selectionSummary = selectionPreviews
      .map((p) => `${p.matchLabel} · ${p.selectionName}`)
      .join('；');
    return { ...row, selectionPreviews, selectionSummary };
  }

  private formatBetListRow(
    b: {
      id: bigint;
      betNo: string;
      userId: bigint;
      agentId: bigint | null;
      betType: string;
      stake: Decimal;
      totalOdds: Decimal | null;
      potentialReturn: Decimal | null;
      actualReturn: Decimal;
      status: string;
      settlementStatus: string | null;
      currency: string;
      placedAt: Date;
      settledAt: Date | null;
      isCashbacked?: boolean;
      user: { id: bigint; username: string; parent: { username: string } | null };
      _count: { selections: number };
    },
  ) {
    return {
      id: b.id.toString(),
      betNo: b.betNo,
      userId: b.userId.toString(),
      username: b.user.username,
      parentUsername: b.user.parent?.username ?? null,
      agentId: b.agentId?.toString() ?? null,
      betType: b.betType,
      stake: this.dec(b.stake),
      totalOdds: b.totalOdds ? this.dec(b.totalOdds) : null,
      potentialReturn: b.potentialReturn ? this.dec(b.potentialReturn) : null,
      actualReturn: this.dec(b.actualReturn),
      status: b.status,
      settlementStatus: b.settlementStatus,
      currency: b.currency,
      placedAt: b.placedAt,
      settledAt: b.settledAt,
      isCashbacked: b.isCashbacked ?? false,
      selectionCount: b._count.selections,
    };
  }

  async listBetsAdmin(params: {
    page?: number;
    pageSize?: number;
    keyword?: string;
    status?: string;
    betType?: string;
    placedFrom?: string;
    placedTo?: string;
  }) {
    const page = Math.max(1, params.page ?? 1);
    const pageSize = Math.min(Math.max(1, params.pageSize ?? 10), 100);
    const skip = (page - 1) * pageSize;

    const where: Prisma.BetWhereInput = {};

    if (params.status) where.status = params.status;
    if (params.betType) where.betType = params.betType;

    if (params.placedFrom || params.placedTo) {
      where.placedAt = {};
      if (params.placedFrom) {
        const from = new Date(params.placedFrom);
        from.setHours(0, 0, 0, 0);
        where.placedAt.gte = from;
      }
      if (params.placedTo) {
        const to = new Date(params.placedTo);
        to.setHours(23, 59, 59, 999);
        where.placedAt.lte = to;
      }
    }

    const kw = params.keyword?.trim();
    if (kw) {
      where.OR = [
        { betNo: { contains: kw, mode: 'insensitive' } },
        { user: { username: { contains: kw, mode: 'insensitive' } } },
      ];
    }

    const [items, total] = await Promise.all([
      this.prisma.bet.findMany({
        where,
        include: {
          user: {
            select: {
              id: true,
              username: true,
              parent: { select: { username: true } },
            },
          },
          selections: { orderBy: { sortOrder: 'asc' } },
        },
        orderBy: { placedAt: 'desc' },
        skip,
        take: pageSize,
      }),
      this.prisma.bet.count({ where }),
    ]);

    const matchIds = [
      ...new Set(
        items.flatMap((b) =>
          b.selections.map((s) => s.matchId).filter((id): id is bigint => id != null),
        ),
      ),
    ];
    const matchContext = await this.loadMatchContext(matchIds);

    return {
      items: items.map((b) =>
        this.attachSelectionPreviews(
          this.formatBetListRow({
            ...b,
            _count: { selections: b.selections.length },
          }),
          b.selections,
          matchContext,
        ),
      ),
      total,
      page,
      pageSize,
    };
  }

  async getBetAdminDetail(betId: bigint) {
    const bet = await this.prisma.bet.findUnique({
      where: { id: betId },
      include: {
        user: {
          select: {
            id: true,
            username: true,
            parent: { select: { username: true } },
          },
        },
        selections: { orderBy: { sortOrder: 'asc' } },
      },
    });
    if (!bet) throw appNotFound('BET_NOT_FOUND');

    const matchIds = bet.selections
      .map((s) => s.matchId)
      .filter((id): id is bigint => id != null);
    const matchContext = await this.loadMatchContext(matchIds);
    const selectionPreviews = this.formatSelectionPreviews(bet.selections, matchContext);

    const base = this.attachSelectionPreviews(
      this.formatBetListRow({
        ...bet,
        _count: { selections: bet.selections.length },
      }),
      bet.selections,
      matchContext,
    );

    return {
      ...base,
      requestId: bet.requestId,
      createdAt: bet.createdAt,
      updatedAt: bet.updatedAt,
      selections: bet.selections.map((s, index) => {
        const preview = selectionPreviews[index];
        return {
          id: s.id.toString(),
          matchId: s.matchId?.toString() ?? null,
          matchLabel: preview?.matchLabel ?? '—',
          leagueName: preview?.leagueName ?? '',
          marketType: s.marketType,
          marketName: s.marketNameSnapshot,
          period: s.period,
          selectionName: s.selectionNameSnapshot,
          handicapLine: s.handicapLine ? this.dec(s.handicapLine) : null,
          totalLine: s.totalLine ? this.dec(s.totalLine) : null,
          odds: this.dec(s.odds),
          resultStatus: s.resultStatus,
          effectiveOdds: s.effectiveOdds ? this.dec(s.effectiveOdds) : null,
          sortOrder: s.sortOrder,
        };
      }),
    };
  }

  private async resolveUserLocale(userId: bigint, client: PrismaClientLike) {
    const userDelegate = (client as PrismaClientLike & {
      user?: {
        findUnique?: PrismaService['user']['findUnique'];
      };
    }).user;
    if (!userDelegate?.findUnique) return 'zh-CN';
    const user = await userDelegate.findUnique({
      where: { id: userId },
      select: { locale: true, preferences: { select: { locale: true } } },
    });
    return user?.preferences?.locale || user?.locale || 'zh-CN';
  }

  private marketNameSnapshot(selection: SelectionWithMarketLabel, locale: string) {
    const market = selection.market;
    return (
      resolveMarketText(market.nameI18n, locale, defaultMarketName(market.marketType, locale)) ||
      market.marketType
    );
  }

  private selectionNameSnapshot(selection: SelectionWithMarketLabel, locale: string) {
    return (
      resolveMarketText(
        selection.nameI18n,
        locale,
        defaultSelectionName(selection.market.marketType, selection.selectionCode, locale) ||
          selection.selectionName,
      ) || selection.selectionName
    );
  }
}
