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 { SystemConfigService } from '../../../shared/config/system-config.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../../shared/common/decorators';
import { appBadRequest } from '../../../shared/common/app-error';
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;
  availableBalance: Decimal;
};

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

@Injectable()
export class CashbackService {
  constructor(
    private prisma: PrismaService,
    private funds: FundsPostingService,
    private systemConfig: SystemConfigService,
  ) {}

  /** 已被待发放/已发放返水批次占用的注单 */
  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, platformDirectRateRaw] =
      await Promise.all([
      this.prisma.bet.findMany({
        where: {
          status: { in: ['WON', 'LOST'] },
          settledAt: { gte: periodStart, lte: periodEnd },
        },
        include: {
          user: { select: { id: true, parentId: true, inviteSponsorId: true } },
          selections: { select: { marketType: true } },
        },
      }),
      this.prisma.cashbackRule.findMany({ where: { isActive: true } }),
      this.prisma.agentProfile.findMany({
        select: { userId: true, cashbackRate: true },
      }),
      this.loadClaimedBetIds(),
      this.systemConfig.getPlatformDirectCashbackSettings(),
    ]);

    const platformDirectDefaultRate = new Decimal(platformDirectRateRaw.platformDirectRate);
    const adminInviteDefaultRate = new Decimal(platformDirectRateRaw.adminInviteRate);

    const agentRateById = new Map(
      agentProfiles.map((p) => [p.userId.toString(), new Decimal(p.cashbackRate)]),
    );
    const sponsorTypeById = new Map<string, string>();
    const sponsorIds = [
      ...new Set(
        settledBets
          .map((b) => b.user.inviteSponsorId)
          .filter((id): id is bigint => id != null),
      ),
    ];
    if (sponsorIds.length > 0) {
      const sponsors = await this.prisma.user.findMany({
        where: { id: { in: sponsorIds } },
        select: { id: true, userType: true },
      });
      for (const sponsor of sponsors) {
        sponsorTypeById.set(sponsor.id.toString(), sponsor.userType);
      }
    }

    const resolveDefaultRate = (user: {
      parentId: bigint | null;
      inviteSponsorId: bigint | null;
    }) => {
      if (user.parentId) {
        return agentRateById.get(user.parentId.toString()) ?? new Decimal(0);
      }
      if (user.inviteSponsorId) {
        const sponsorType = sponsorTypeById.get(user.inviteSponsorId.toString());
        if (sponsorType === 'ADMIN') return adminInviteDefaultRate;
      }
      return platformDirectDefaultRate;
    };
    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 = resolveDefaultRate(bet.user);
      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 wallets =
      userIds.length > 0
        ? await this.prisma.wallet.findMany({
            where: { userId: { in: userIds } },
            select: { userId: true, availableBalance: true },
          })
        : [];
    const balanceByUserId = new Map(
      wallets.map((w) => [w.userId.toString(), w.availableBalance]),
    );

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

    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 appBadRequest('CASHBACK_DATE_RANGE_INVALID');
    }

    const alreadyPaid = await this.prisma.cashbackBatch.findFirst({
      where: { status: 'CONFIRMED', periodStart: start, periodEnd: end },
    });
    if (alreadyPaid) {
      throw appBadRequest('CASHBACK_ALREADY_ISSUED');
    }

    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 appBadRequest('CASHBACK_BETS_IN_OTHER_BATCH');
      }
      throw appBadRequest('CASHBACK_NO_ELIGIBLE_BETS');
    }

    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 appBadRequest('CASHBACK_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]));

    const wallets =
      userIds.length > 0
        ? await this.prisma.wallet.findMany({
            where: { userId: { in: userIds } },
            select: { userId: true, availableBalance: true },
          })
        : [];
    const balanceByUserId = new Map(
      wallets.map((w) => [w.userId.toString(), w.availableBalance]),
    );

    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,
        availableBalance:
          balanceByUserId.get(item.userId.toString()) ?? new Decimal(0),
        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 appBadRequest('CASHBACK_BATCH_NOT_FOUND');
    if (batch.status !== 'PREVIEW') throw appBadRequest('CASHBACK_BATCH_NOT_ISSUABLE');
    if (batch.items.length === 0 || batch.totalAmount.lte(0)) {
      throw appBadRequest('CASHBACK_NO_AMOUNT');
    }

    const duplicate = await this.prisma.cashbackBatch.findFirst({
      where: {
        status: 'CONFIRMED',
        periodStart: batch.periodStart,
        periodEnd: batch.periodEnd,
        id: { not: batchId },
      },
    });
    if (duplicate) {
      throw appBadRequest('CASHBACK_PERIOD_ALREADY_ISSUED');
    }

    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 appBadRequest('CASHBACK_BETS_ALREADY_PAID');
      }
    }

    await this.prisma.$transaction(async (tx) => {
      for (const item of batch.items) {
        if (item.amount.gt(0)) {
          await this.funds.deposit({
            userId: item.userId,
            amount: item.amount,
            operatorId,
            remark: `Cashback batch ${batch.batchNo}`,
            referenceId: batch.batchNo,
            transactionType: 'CASHBACK_DEPOSIT',
            tx,
            businessKey: `cashback:${batch.batchNo}:${item.userId}`,
          });
        }
      }

      if (betIds.length > 0) {
        await tx.bet.updateMany({
          where: { id: { in: betIds } },
          data: { isCashbacked: true },
        });
      }

      await tx.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 appBadRequest('CASHBACK_BATCH_NOT_FOUND');
    if (batch.status !== 'PREVIEW') {
      throw appBadRequest('CASHBACK_PREVIEW_ONLY_VOID');
    }

    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,
    }));
  }

  async getPlayerCustomCashbackRate(userId: bigint): Promise<Decimal | null> {
    const rule = await this.prisma.cashbackRule.findFirst({
      where: {
        targetType: 'USER',
        targetId: userId,
        isActive: true,
        marketType: null,
      },
      orderBy: { updatedAt: 'desc' },
    });
    if (!rule) return null;
    const rate = new Decimal(rule.rate);
    return rate.gt(0) ? rate : null;
  }

  async setPlayerCustomCashbackRate(userId: bigint, rate: Decimal | null) {
    await this.prisma.$transaction(async (tx) => {
      await tx.cashbackRule.updateMany({
        where: { targetType: 'USER', targetId: userId },
        data: { isActive: false },
      });
      if (rate && rate.gt(0)) {
        await tx.cashbackRule.create({
          data: {
            name: `Player ${userId.toString()}`,
            targetType: 'USER',
            targetId: userId,
            rate,
            isActive: true,
          },
        });
      }
    });
  }

  async resolvePlayerDefaultCashbackRate(params: {
    parentId: bigint | null;
    inviteSponsorId?: bigint | null;
  }): Promise<Decimal> {
    if (params.parentId) {
      const profile = await this.prisma.agentProfile.findUnique({
        where: { userId: params.parentId },
        select: { cashbackRate: true },
      });
      return profile ? new Decimal(profile.cashbackRate) : new Decimal(0);
    }

    const settings = await this.systemConfig.getPlatformDirectCashbackSettings();
    if (params.inviteSponsorId) {
      const sponsor = await this.prisma.user.findUnique({
        where: { id: params.inviteSponsorId },
        select: { userType: true },
      });
      if (sponsor?.userType === 'ADMIN') {
        return new Decimal(settings.adminInviteRate);
      }
    }
    return new Decimal(settings.platformDirectRate);
  }
}
