import { Injectable, BadRequestException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../../shared/prisma/prisma.service';
import { WalletService } from '../../ledger/wallet.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../../shared/common/decorators';
import {
  resolveCashbackRateForBet,
  type CashbackRuleRow,
} from './cashback-rate.resolver';

type AggregatedItem = {
  userId: bigint;
  effectiveStake: Decimal;
  betCount: number;
  rate: Decimal;
  amount: Decimal;
  username: string;
  agentUsername: string | null;
};

type BetCashbackLine = {
  betId: bigint;
  userId: bigint;
  stake: Decimal;
  amount: Decimal;
};

@Injectable()
export class CashbackService {
  constructor(
    private prisma: PrismaService,
    private wallet: WalletService,
  ) {}

  /** 已被待发放/已发放返水批次占用的注单 */
  private async loadClaimedBetIds(): Promise<Set<string>> {
    const rows = await this.prisma.cashbackBet.findMany({
      where: {
        batch: { status: { in: ['PREVIEW', 'CONFIRMED'] } },
      },
      select: { betId: true },
    });
    return new Set(rows.map((r) => r.betId.toString()));
  }

  private async aggregatePeriod(periodStart: Date, periodEnd: Date): Promise<{
    items: AggregatedItem[];
    betLines: BetCashbackLine[];
    totalAmount: Decimal;
    totalEffectiveStake: Decimal;
    totalBetCount: number;
    eligibleBetCount: number;
    skippedClaimedCount: number;
  }> {
    const [settledBets, rules, agentProfiles, claimedBetIds] = await Promise.all([
      this.prisma.bet.findMany({
        where: {
          status: { in: ['WON', 'LOST'] },
          settledAt: { gte: periodStart, lte: periodEnd },
        },
        include: {
          user: { select: { id: true, parentId: true } },
          selections: { select: { marketType: true } },
        },
      }),
      this.prisma.cashbackRule.findMany({ where: { isActive: true } }),
      this.prisma.agentProfile.findMany({
        select: { userId: true, cashbackRate: true },
      }),
      this.loadClaimedBetIds(),
    ]);

    const agentRateById = new Map(
      agentProfiles.map((p) => [p.userId.toString(), new Decimal(p.cashbackRate)]),
    );
    const ruleRows: CashbackRuleRow[] = rules.map((r) => ({
      targetType: r.targetType,
      targetId: r.targetId,
      rate: new Decimal(r.rate),
      marketType: r.marketType,
    }));

    const playerAgg = new Map<
      string,
      { userId: bigint; stake: Decimal; amount: Decimal; betCount: number }
    >();
    const betLines: BetCashbackLine[] = [];
    let eligibleBetCount = 0;
    let skippedClaimedCount = 0;

    for (const bet of settledBets) {
      const agentId = bet.user.parentId;
      const agentDefaultRate = agentId
        ? agentRateById.get(agentId.toString()) ?? new Decimal(0)
        : new Decimal(0);
      const marketTypes = bet.selections.map((s) => s.marketType);
      const rate = resolveCashbackRateForBet({
        userId: bet.userId,
        agentId,
        marketTypes,
        agentDefaultRate,
        rules: ruleRows,
      });

      if (rate.lte(0)) continue;

      eligibleBetCount += 1;

      if (claimedBetIds.has(bet.id.toString())) {
        skippedClaimedCount += 1;
        continue;
      }

      const lineAmount = bet.stake.mul(rate);
      betLines.push({
        betId: bet.id,
        userId: bet.userId,
        stake: bet.stake,
        amount: lineAmount,
      });

      const key = bet.userId.toString();
      const existing = playerAgg.get(key) ?? {
        userId: bet.userId,
        stake: new Decimal(0),
        amount: new Decimal(0),
        betCount: 0,
      };
      existing.stake = existing.stake.add(bet.stake);
      existing.amount = existing.amount.add(lineAmount);
      existing.betCount += 1;
      playerAgg.set(key, existing);
    }

    const rawItems = Array.from(playerAgg.values())
      .map((p) => ({
        userId: p.userId,
        effectiveStake: p.stake,
        betCount: p.betCount,
        rate: p.stake.gt(0) ? p.amount.div(p.stake) : new Decimal(0),
        amount: p.amount,
      }))
      .sort((a, b) => (b.amount.gt(a.amount) ? 1 : a.amount.gt(b.amount) ? -1 : 0));

    const userIds = rawItems.map((i) => i.userId);
    const users =
      userIds.length > 0
        ? await this.prisma.user.findMany({
            where: { id: { in: userIds } },
            select: {
              id: true,
              username: true,
              parent: { select: { username: true } },
            },
          })
        : [];
    const userById = new Map(users.map((u) => [u.id.toString(), u]));

    const items: AggregatedItem[] = rawItems.map((item) => {
      const user = userById.get(item.userId.toString());
      return {
        ...item,
        username: user?.username ?? '',
        agentUsername: user?.parent?.username ?? null,
      };
    });

    const totalAmount = items.reduce((s, i) => s.add(i.amount), new Decimal(0));
    const totalEffectiveStake = items.reduce((s, i) => s.add(i.effectiveStake), new Decimal(0));
    const totalBetCount = items.reduce((s, i) => s + i.betCount, 0);

    return {
      items,
      betLines,
      totalAmount,
      totalEffectiveStake,
      totalBetCount,
      eligibleBetCount,
      skippedClaimedCount,
    };
  }

  private normalizePeriodStart(input: Date): Date {
    const d = new Date(input);
    d.setHours(0, 0, 0, 0);
    return d;
  }

  private normalizePeriodEnd(input: Date): Date {
    const d = new Date(input);
    d.setHours(23, 59, 59, 999);
    return d;
  }

  private async removePreviewBatchesForPeriod(
    periodStart: Date,
    periodEnd: Date,
    tx: Prisma.TransactionClient,
  ) {
    const stale = await tx.cashbackBatch.findMany({
      where: { status: 'PREVIEW', periodStart, periodEnd },
      select: { id: true },
    });
    for (const b of stale) {
      await tx.cashbackBet.deleteMany({ where: { batchId: b.id } });
      await tx.cashbackItem.deleteMany({ where: { batchId: b.id } });
      await tx.cashbackBatch.delete({ where: { id: b.id } });
    }
    return stale.length;
  }

  async previewBatch(periodStart: Date, periodEnd: Date) {
    const start = this.normalizePeriodStart(periodStart);
    const end = this.normalizePeriodEnd(periodEnd);
    if (start > end) {
      throw new BadRequestException('开始日期不能晚于结束日期');
    }

    const alreadyPaid = await this.prisma.cashbackBatch.findFirst({
      where: { status: 'CONFIRMED', periodStart: start, periodEnd: end },
    });
    if (alreadyPaid) {
      throw new BadRequestException('该统计周期已发放返水，不可重复生成预览');
    }

    const {
      items,
      betLines,
      totalAmount,
      totalEffectiveStake,
      totalBetCount,
      eligibleBetCount,
      skippedClaimedCount,
    } = await this.aggregatePeriod(start, end);

    if (items.length === 0 || totalAmount.lte(0)) {
      if (eligibleBetCount > 0 && skippedClaimedCount >= eligibleBetCount) {
        throw new BadRequestException('该周期内的有效注单均已计入其他返水批次，无法生成预览');
      }
      throw new BadRequestException('该周期内无符合条件的返水，无法生成预览');
    }

    let batch!: Awaited<ReturnType<typeof this.prisma.cashbackBatch.create>>;
    const replacedPreviewCount = await this.prisma.$transaction(async (tx) => {
      const replaced = await this.removePreviewBatchesForPeriod(start, end, tx);

      batch = await tx.cashbackBatch.create({
        data: {
          batchNo: generateBatchNo('CB'),
          periodStart: start,
          periodEnd: end,
          status: 'PREVIEW',
          totalAmount,
          totalEffectiveStake,
          totalBetCount,
          playerCount: items.length,
        },
      });

      for (const item of items) {
        await tx.cashbackItem.create({
          data: {
            batchId: batch.id,
            userId: item.userId,
            effectiveStake: item.effectiveStake,
            betCount: item.betCount,
            rate: item.rate,
            amount: item.amount,
          },
        });
      }

      for (const line of betLines) {
        await tx.cashbackBet.create({
          data: {
            batchId: batch.id,
            betId: line.betId,
            userId: line.userId,
            stake: line.stake,
            amount: line.amount,
          },
        });
      }

      return replaced;
    });

    const avgRate = totalEffectiveStake.gt(0)
      ? totalAmount.div(totalEffectiveStake)
      : new Decimal(0);

    return {
      batch,
      items,
      totalAmount,
      totalEffectiveStake,
      totalBetCount,
      avgRate,
      replacedPreviewCount,
    };
  }

  async listBatches(params: { page: number; pageSize: number; status?: string }) {
    const page = Math.max(1, params.page);
    const pageSize = Math.min(100, Math.max(1, params.pageSize));
    const where = params.status?.trim() ? { status: params.status.trim() } : {};

    const [rows, total] = await Promise.all([
      this.prisma.cashbackBatch.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
      this.prisma.cashbackBatch.count({ where }),
    ]);

    const operatorIds = rows
      .map((r) => r.operatorId)
      .filter((id): id is bigint => id != null);
    const operators =
      operatorIds.length > 0
        ? await this.prisma.user.findMany({
            where: { id: { in: operatorIds } },
            select: { id: true, username: true },
          })
        : [];
    const operatorById = new Map(operators.map((u) => [u.id.toString(), u.username]));

    const items = rows.map((row) => ({
      ...row,
      operatorUsername: row.operatorId
        ? operatorById.get(row.operatorId.toString()) ?? null
        : null,
    }));

    return { items, total, page, pageSize };
  }

  async getBatchDetail(batchId: bigint) {
    const batch = await this.prisma.cashbackBatch.findUnique({
      where: { id: batchId },
      include: {
        items: { orderBy: { amount: 'desc' } },
      },
    });
    if (!batch) throw new BadRequestException('Batch not found');

    const userIds = batch.items.map((i) => i.userId);
    const users =
      userIds.length > 0
        ? await this.prisma.user.findMany({
            where: { id: { in: userIds } },
            select: {
              id: true,
              username: true,
              parent: { select: { username: true } },
            },
          })
        : [];
    const userById = new Map(users.map((u) => [u.id.toString(), u]));

    let operatorUsername: string | null = null;
    if (batch.operatorId) {
      const op = await this.prisma.user.findUnique({
        where: { id: batch.operatorId },
        select: { username: true },
      });
      operatorUsername = op?.username ?? null;
    }

    const items = batch.items.map((item) => {
      const user = userById.get(item.userId.toString());
      return {
        id: item.id,
        userId: item.userId,
        username: user?.username ?? '',
        agentUsername: user?.parent?.username ?? null,
        effectiveStake: item.effectiveStake,
        betCount: item.betCount,
        rate: item.rate,
        amount: item.amount,
      };
    });

    const avgRate = batch.totalEffectiveStake.gt(0)
      ? batch.totalAmount.div(batch.totalEffectiveStake)
      : new Decimal(0);

    return {
      batch: { ...batch, operatorUsername },
      items,
      totalAmount: batch.totalAmount,
      totalEffectiveStake: batch.totalEffectiveStake,
      totalBetCount: batch.totalBetCount,
      avgRate,
    };
  }

  async confirmBatch(batchId: bigint, operatorId: bigint) {
    const batch = await this.prisma.cashbackBatch.findUnique({
      where: { id: batchId },
      include: { items: true, bets: true },
    });
    if (!batch) throw new BadRequestException('Batch not found');
    if (batch.status !== 'PREVIEW') throw new BadRequestException('该批次不可发放');
    if (batch.items.length === 0 || batch.totalAmount.lte(0)) {
      throw new BadRequestException('批次无有效返水金额');
    }

    const duplicate = await this.prisma.cashbackBatch.findFirst({
      where: {
        status: 'CONFIRMED',
        periodStart: batch.periodStart,
        periodEnd: batch.periodEnd,
        id: { not: batchId },
      },
    });
    if (duplicate) {
      throw new BadRequestException('该统计周期已发放返水');
    }

    const betIds = batch.bets.map((b) => b.betId);
    if (betIds.length > 0) {
      const conflict = await this.prisma.cashbackBet.findFirst({
        where: {
          betId: { in: betIds },
          batch: { status: 'CONFIRMED' },
        },
      });
      if (conflict) {
        throw new BadRequestException('部分注单已在其他批次发放返水，请作废本预览后重新生成');
      }
    }

    for (const item of batch.items) {
      if (item.amount.gt(0)) {
        await this.wallet.deposit(
          item.userId,
          item.amount,
          operatorId,
          `Cashback batch ${batch.batchNo}`,
          batch.batchNo,
          'CASHBACK_DEPOSIT',
        );
      }
    }

    await this.prisma.cashbackBatch.update({
      where: { id: batchId },
      data: { status: 'CONFIRMED', confirmedAt: new Date(), operatorId },
    });

    return { success: true };
  }

  async cancelBatch(batchId: bigint) {
    const batch = await this.prisma.cashbackBatch.findUnique({ where: { id: batchId } });
    if (!batch) throw new BadRequestException('Batch not found');
    if (batch.status !== 'PREVIEW') {
      throw new BadRequestException('只能作废待发放批次');
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.cashbackBet.deleteMany({ where: { batchId } });
      await tx.cashbackBatch.update({
        where: { id: batchId },
        data: { status: 'CANCELLED' },
      });
    });

    return { success: true };
  }

  async getUserCashbacks(userId: bigint) {
    const items = await this.prisma.cashbackItem.findMany({
      where: { userId, batch: { status: 'CONFIRMED' } },
      include: {
        batch: {
          select: {
            batchNo: true,
            periodStart: true,
            periodEnd: true,
            confirmedAt: true,
            status: true,
          },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    return items.map((item) => ({
      id: item.id.toString(),
      batchNo: item.batch.batchNo,
      periodStart: item.batch.periodStart,
      periodEnd: item.batch.periodEnd,
      confirmedAt: item.batch.confirmedAt,
      effectiveStake: item.effectiveStake.toString(),
      betCount: item.betCount,
      rate: item.rate.toString(),
      amount: item.amount.toString(),
      createdAt: item.createdAt,
    }));
  }
}
