import { Injectable } from '@nestjs/common';
import { appBadRequest } from '../../shared/common/app-error';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateTransactionId } from '../../shared/common/decorators';

type TxClient = Prisma.TransactionClient;

type WalletPostingOptions = {
  tx?: TxClient;
  businessKey?: string;
};

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

  async getWallet(userId: bigint) {
    const wallet = await this.prisma.wallet.findUnique({ where: { userId } });
    if (!wallet) throw appBadRequest('WALLET_NOT_FOUND');
    return wallet;
  }

  async createWallet(userId: bigint, currency = 'USD') {
    return this.prisma.wallet.create({
      data: { userId, currency },
    });
  }

  private async lockWallet(tx: TxClient, userId: bigint) {
    const wallets = await tx.$queryRaw<Array<{ id: bigint; available_balance: Decimal; frozen_balance: Decimal; version: number }>>`
      SELECT id, available_balance, frozen_balance, version FROM wallets WHERE user_id = ${userId} FOR UPDATE
    `;
    if (!wallets.length) throw appBadRequest('WALLET_NOT_FOUND');
    return wallets[0];
  }

  private async findPosting(client: TxClient, businessKey?: string | null) {
    if (!businessKey) return null;
    return client.walletTransaction.findUnique({ where: { businessKey } });
  }

  async deposit(
    userId: bigint,
    amount: Decimal | number,
    operatorId: bigint,
    remark?: string,
    referenceId?: string,
    transactionType = 'MANUAL_DEPOSIT',
    tx?: TxClient,
    businessKey?: string,
  ) {
    const amt = new Decimal(amount);
    if (amt.lte(0)) throw appBadRequest('AMOUNT_MUST_BE_POSITIVE');

    const run = async (client: TxClient) => {
      const existing = await this.findPosting(client, businessKey);
      if (existing) return { balanceAfter: existing.balanceAfter };

      const w = await this.lockWallet(client, userId);
      const afterLockExisting = await this.findPosting(client, businessKey);
      if (afterLockExisting) return { balanceAfter: afterLockExisting.balanceAfter };

      const balanceBefore = new Decimal(w.available_balance);
      const balanceAfter = balanceBefore.add(amt);

      await client.wallet.update({
        where: { id: w.id },
        data: {
          availableBalance: balanceAfter,
          version: { increment: 1 },
        },
      });

      await client.walletTransaction.create({
        data: {
          transactionId: generateTransactionId(),
          userId,
          walletId: w.id,
          transactionType,
          amount: amt,
          balanceBefore,
          balanceAfter,
          frozenBefore: w.frozen_balance,
          frozenAfter: w.frozen_balance,
          referenceType: 'DEPOSIT',
          referenceId,
          businessKey,
          operatorId,
          remark,
        },
      });

      return { balanceAfter };
    };

    if (tx) return run(tx);
    return this.prisma.$transaction(run);
  }

  async withdraw(
    userId: bigint,
    amount: Decimal | number,
    operatorId: bigint,
    remark?: string,
    referenceId?: string,
    transactionType = 'MANUAL_WITHDRAW',
    tx?: TxClient,
    businessKey?: string,
    referenceType = 'WITHDRAW',
  ) {
    const amt = new Decimal(amount);
    if (amt.lte(0)) throw appBadRequest('AMOUNT_MUST_BE_POSITIVE');

    const run = async (client: TxClient) => {
      const existing = await this.findPosting(client, businessKey);
      if (existing) return { balanceAfter: existing.balanceAfter };

      const w = await this.lockWallet(client, userId);
      const afterLockExisting = await this.findPosting(client, businessKey);
      if (afterLockExisting) return { balanceAfter: afterLockExisting.balanceAfter };

      const balanceBefore = new Decimal(w.available_balance);
      if (balanceBefore.lt(amt)) throw appBadRequest('INSUFFICIENT_BALANCE');
      const balanceAfter = balanceBefore.sub(amt);

      await client.wallet.update({
        where: { id: w.id },
        data: {
          availableBalance: balanceAfter,
          version: { increment: 1 },
        },
      });

      await client.walletTransaction.create({
        data: {
          transactionId: generateTransactionId(),
          userId,
          walletId: w.id,
          transactionType,
          amount: amt.neg(),
          balanceBefore,
          balanceAfter,
          frozenBefore: w.frozen_balance,
          frozenAfter: w.frozen_balance,
          referenceType,
          referenceId,
          businessKey,
          operatorId,
          remark,
        },
      });

      return { balanceAfter };
    };

    if (tx) return run(tx);
    return this.prisma.$transaction(run);
  }

  async freezeForBet(
    userId: bigint,
    stake: Decimal | number,
    betId: string,
    tx?: TxClient,
    businessKey = `bet:${betId}:freeze`,
  ) {
    const amt = new Decimal(stake);

    const run = async (client: TxClient) => {
      const existing = await this.findPosting(client, businessKey);
      if (existing) return;

      const w = await this.lockWallet(client, userId);
      const afterLockExisting = await this.findPosting(client, businessKey);
      if (afterLockExisting) return;

      const avail = new Decimal(w.available_balance);
      if (avail.lt(amt)) throw appBadRequest('INSUFFICIENT_BALANCE');

      const balanceAfter = avail.sub(amt);
      const frozenAfter = new Decimal(w.frozen_balance).add(amt);

      await client.wallet.update({
        where: { id: w.id },
        data: {
          availableBalance: balanceAfter,
          frozenBalance: frozenAfter,
          version: { increment: 1 },
        },
      });

      await client.walletTransaction.create({
        data: {
          transactionId: generateTransactionId(),
          userId,
          walletId: w.id,
          transactionType: 'BET_FREEZE',
          amount: amt.neg(),
          balanceBefore: avail,
          balanceAfter,
          frozenBefore: w.frozen_balance,
          frozenAfter,
          referenceType: 'BET',
          referenceId: betId,
          businessKey,
        },
      });
    };

    if (tx) return run(tx);
    return this.prisma.$transaction(run);
  }

  async settleBet(
    userId: bigint,
    stake: Decimal,
    payout: Decimal,
    betId: string,
    result: 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE',
    tx?: TxClient,
    businessKey?: string,
  ) {
    const run = async (client: TxClient) => {
      const existing = await this.findPosting(client, businessKey);
      if (existing) return;

      const txTypeMap: Record<string, string> = {
        WIN: 'BET_SETTLE_WIN',
        LOSE: 'BET_SETTLE_LOSE',
        PUSH: 'BET_SETTLE_PUSH',
        VOID: 'BET_VOID_REFUND',
        HALF_WIN: 'BET_SETTLE_WIN',
        HALF_LOSE: 'BET_SETTLE_LOSE',
      };

      const w = await this.lockWallet(client, userId);
      const afterLockExisting = await this.findPosting(client, businessKey);
      if (afterLockExisting) return;

      const avail = new Decimal(w.available_balance);
      const frozen = new Decimal(w.frozen_balance);
      const frozenAfter = frozen.sub(stake);
      if (frozenAfter.lt(0)) {
        throw appBadRequest('WALLET_FROZEN_INSUFFICIENT');
      }
      const balanceAfter = avail.add(payout);

      await client.wallet.update({
        where: { id: w.id },
        data: {
          availableBalance: balanceAfter,
          frozenBalance: frozenAfter,
          version: { increment: 1 },
        },
      });

      await client.walletTransaction.create({
        data: {
          transactionId: generateTransactionId(),
          userId,
          walletId: w.id,
          transactionType: txTypeMap[result] || 'BET_SETTLE_WIN',
          amount: payout,
          balanceBefore: avail,
          balanceAfter,
          frozenBefore: frozen,
          frozenAfter,
          referenceType: 'BET',
          referenceId: betId,
          businessKey,
        },
      });
    };

    if (tx) return run(tx);
    return this.prisma.$transaction(run);
  }

  /** 重结算差额调整：delta = 新派彩 - 原派彩，允许扣回导致负余额 */
  async applyResettleDelta(
    userId: bigint,
    delta: Decimal,
    betNo: string,
    tx?: TxClient,
    businessKey?: string,
  ) {
    if (delta.eq(0)) return;

    const run = async (client: TxClient) => {
      const existing = await this.findPosting(client, businessKey);
      if (existing) return;

      const w = await this.lockWallet(client, userId);
      const afterLockExisting = await this.findPosting(client, businessKey);
      if (afterLockExisting) return;

      const avail = new Decimal(w.available_balance);
      const balanceAfter = avail.add(delta);
      const txType = delta.gt(0) ? 'BET_SETTLE_WIN' : 'RESETTLE_REVERSE';

      await client.wallet.update({
        where: { id: w.id },
        data: {
          availableBalance: balanceAfter,
          version: { increment: 1 },
        },
      });

      await client.walletTransaction.create({
        data: {
          transactionId: generateTransactionId(),
          userId,
          walletId: w.id,
          transactionType: txType,
          amount: delta,
          balanceBefore: avail,
          balanceAfter,
          frozenBefore: w.frozen_balance,
          frozenAfter: w.frozen_balance,
          referenceType: 'BET',
          referenceId: betNo,
          businessKey,
          remark: 'Resettlement adjustment',
        },
      });
    };

    if (tx) return run(tx);
    return this.prisma.$transaction(run);
  }

  private static readonly DEPOSIT_TX_TYPES = [
    'MANUAL_DEPOSIT',
    'ADMIN_DEPOSIT',
    'AGENT_DEPOSIT',
    'INITIAL_DEPOSIT',
    'DEPOSIT',
    'MANUAL_ADJUST',
    'PLAYER_DEPOSIT',
  ] as const;

  private static readonly WITHDRAW_TX_TYPES = [
    'MANUAL_WITHDRAW',
    'ADMIN_WITHDRAW',
    'AGENT_WITHDRAW',
    'WITHDRAW',
  ] as const;

  private static readonly SYSTEM_REMARKS = new Set([
    '管理员上分',
    '管理员下分',
    '代理上分',
    '代理下分',
    '开户初始余额',
    'Resettlement adjustment',
  ]);

  private resolveDisplayType(
    transactionType: string,
    operatorType?: string | null,
  ): string {
    const type = transactionType.toUpperCase();
    if (type === 'INITIAL_DEPOSIT') {
      if (operatorType === 'AGENT') return 'AGENT_DEPOSIT';
      return 'ADMIN_DEPOSIT';
    }
    if (
      [
        'ADMIN_DEPOSIT',
        'AGENT_DEPOSIT',
        'ADMIN_WITHDRAW',
        'AGENT_WITHDRAW',
        'PLAYER_DEPOSIT',
        'PLAYER_DEPOSIT_REVERSAL',
      ].includes(type)
    ) {
      return type;
    }
    if (type === 'MANUAL_DEPOSIT') {
      if (operatorType === 'ADMIN') return 'ADMIN_DEPOSIT';
      if (operatorType === 'AGENT') return 'AGENT_DEPOSIT';
      return type;
    }
    if (type === 'MANUAL_WITHDRAW') {
      if (operatorType === 'ADMIN') return 'ADMIN_WITHDRAW';
      if (operatorType === 'AGENT') return 'AGENT_WITHDRAW';
      return type;
    }
    return type;
  }

  private isCustomRemark(remark: string | null | undefined): boolean {
    const r = remark?.trim();
    if (!r) return false;
    if (WalletService.SYSTEM_REMARKS.has(r)) return false;
    if (r.startsWith('Cashback batch ')) return false;
    if (r.startsWith('Deposit order ')) return false;
    if (r.startsWith('Revoke approved deposit ')) return false;
    return true;
  }

  private buildPlayerTxSummary(
    tx: {
      transactionType: string;
      referenceType: string | null;
      referenceId: string | null;
      remark: string | null;
    },
    depositMethodName?: string | null,
  ): string | null {
    const type = tx.transactionType.toUpperCase();
    if (tx.referenceType === 'BET' && tx.referenceId) {
      return tx.referenceId;
    }
    if (type === 'CASHBACK' || type === 'CASHBACK_DEPOSIT') {
      return tx.referenceId ?? null;
    }
    if (type === 'PLAYER_DEPOSIT') {
      const parts = [depositMethodName?.trim(), tx.referenceId?.trim()].filter(Boolean);
      return parts.length ? parts.join(' · ') : null;
    }
    if (type === 'PLAYER_DEPOSIT_REVERSAL' && tx.referenceId) {
      return tx.referenceId;
    }
    if (this.isCustomRemark(tx.remark)) return tx.remark!.trim();
    return null;
  }

  private resolveSummaryKind(remark: string | null | undefined): 'opening_bonus' | null {
    const r = remark?.trim();
    if (r === '开户初始余额') return 'opening_bonus';
    return null;
  }

  private async enrichPlayerTransactions(
    rows: Array<{
      id: bigint;
      transactionId: string;
      transactionType: string;
      amount: Decimal;
      balanceBefore: Decimal;
      balanceAfter: Decimal;
      frozenBefore: Decimal;
      frozenAfter: Decimal;
      referenceType: string | null;
      referenceId: string | null;
      remark: string | null;
      operatorId: bigint | null;
      createdAt: Date;
    }>,
  ) {
    const operatorIds = [
      ...new Set(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, userType: true },
          })
        : [];
    const operatorTypeById = new Map(operators.map((o) => [o.id.toString(), o.userType]));

    const depositMethodByRowId = await this.resolveDepositMethodsForRows(rows);

    return rows.map((row) => {
      const operatorType = row.operatorId
        ? operatorTypeById.get(row.operatorId.toString())
        : null;
      const displayType = this.resolveDisplayType(row.transactionType, operatorType);
      const depositMethod = depositMethodByRowId.get(row.id.toString());
      const summary = this.buildPlayerTxSummary(
        row,
        depositMethod?.depositMethodName,
      );
      const summaryKind = summary ? null : this.resolveSummaryKind(row.remark);
      return {
        transactionId: row.transactionId,
        transactionType: row.transactionType,
        displayType,
        summaryKind,
        amount: row.amount.toString(),
        balanceBefore: row.balanceBefore.toString(),
        balanceAfter: row.balanceAfter.toString(),
        frozenBefore: row.frozenBefore.toString(),
        frozenAfter: row.frozenAfter.toString(),
        referenceType: row.referenceType,
        referenceId: row.referenceId,
        remark: row.remark,
        summary,
        createdAt: row.createdAt.toISOString(),
        betNo: row.referenceType === 'BET' ? row.referenceId : null,
        cashbackBatchNo:
          row.transactionType === 'CASHBACK' || row.transactionType === 'CASHBACK_DEPOSIT'
            ? row.referenceId
            : null,
      };
    });
  }

  async getTransactionDetail(userId: bigint, transactionId: string) {
    const tx = await this.prisma.walletTransaction.findFirst({
      where: { userId, transactionId },
    });
    if (!tx) return null;

    const [enriched] = await this.enrichPlayerTransactions([tx]);
    return enriched;
  }

  async getTransactions(userId: bigint, page = 1, pageSize = 20, typeFilter?: string) {
    const skip = (page - 1) * pageSize;

    let typeWhere: Record<string, unknown> = {};
    if (typeFilter === 'deposit') {
      typeWhere = { transactionType: { in: [...WalletService.DEPOSIT_TX_TYPES] } };
    } else if (typeFilter === 'withdraw') {
      typeWhere = { transactionType: { in: [...WalletService.WITHDRAW_TX_TYPES] } };
    } else if (typeFilter === 'bet') {
      typeWhere = { transactionType: { in: ['BET_FREEZE', 'BET_DEDUCT', 'BET_SETTLE_WIN', 'BET_SETTLE_LOSE', 'BET_SETTLE_PUSH', 'BET_WIN', 'BET_REFUND', 'BET_VOID', 'BET_VOID_REFUND', 'RESETTLE_REVERSE'] } };
    } else if (typeFilter === 'cashback') {
      typeWhere = { transactionType: { in: ['CASHBACK', 'CASHBACK_DEPOSIT'] } };
    }

    const where = { userId, ...typeWhere };
    const [items, total] = await Promise.all([
      this.prisma.walletTransaction.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: pageSize,
      }),
      this.prisma.walletTransaction.count({ where }),
    ]);
    return {
      items: await this.enrichPlayerTransactions(items),
      total,
      page,
      pageSize,
    };
  }

  private walletTypeCategoryWhere(category?: string): Prisma.WalletTransactionWhereInput {
    const cat = category?.trim();
    if (cat === 'deposit') {
      return { transactionType: { in: [...WalletService.DEPOSIT_TX_TYPES] } };
    }
    if (cat === 'withdraw') {
      return { transactionType: { in: [...WalletService.WITHDRAW_TX_TYPES] } };
    }
    if (cat === 'bet') {
      return {
        transactionType: {
          in: [
            'BET_FREEZE',
            'BET_DEDUCT',
            'BET_SETTLE_WIN',
            'BET_SETTLE_LOSE',
            'BET_SETTLE_PUSH',
            'BET_WIN',
            'BET_REFUND',
            'BET_VOID',
            'BET_VOID_REFUND',
            'RESETTLE_REVERSE',
          ],
        },
      };
    }
    if (cat === 'cashback') {
      return { transactionType: { in: ['CASHBACK', 'CASHBACK_DEPOSIT'] } };
    }
    return {};
  }

  private static readonly DEPOSIT_RECHARGE_TYPES = new Set([
    'MANUAL_DEPOSIT',
    'ADMIN_DEPOSIT',
    'AGENT_DEPOSIT',
    'INITIAL_DEPOSIT',
    'DEPOSIT',
    'PLAYER_DEPOSIT',
  ]);

  private paymentMethodDisplayName(pm: {
    displayName: string | null;
    bankName: string | null;
    usdtAddress: string | null;
    methodType: string;
  } | null | undefined): string | null {
    if (!pm) return null;
    return pm.displayName ?? pm.bankName ?? pm.usdtAddress ?? pm.methodType ?? null;
  }

  private async resolveDepositMethodsForRows(
    rows: Array<{ id: bigint; transactionType: string; referenceId: string | null; operatorId: bigint | null }>,
  ) {
    const rechargeRows = rows.filter((r) => WalletService.DEPOSIT_RECHARGE_TYPES.has(r.transactionType));
    const result = new Map<
      string,
      { depositMethodKey: string | null; depositMethodName: string | null }
    >();

    if (!rechargeRows.length) return result;

    const orderNos = [
      ...new Set(
        rechargeRows
          .filter((r) => r.transactionType === 'PLAYER_DEPOSIT' && r.referenceId)
          .map((r) => r.referenceId!),
      ),
    ];
    const manualOperatorIds = [
      ...new Set(
        rechargeRows
          .filter((r) => r.transactionType !== 'PLAYER_DEPOSIT' && r.operatorId)
          .map((r) => r.operatorId!),
      ),
    ];

    const [orders, operators] = await Promise.all([
      orderNos.length
        ? this.prisma.depositOrder.findMany({
            where: { orderNo: { in: orderNos } },
            include: {
              paymentMethod: {
                select: {
                  displayName: true,
                  bankName: true,
                  usdtAddress: true,
                  methodType: true,
                },
              },
            },
          })
        : [],
      manualOperatorIds.length
        ? this.prisma.user.findMany({
            where: { id: { in: manualOperatorIds } },
            select: { id: true, userType: true },
          })
        : [],
    ]);

    const orderByNo = new Map(orders.map((o) => [o.orderNo, o]));
    const operatorTypeById = new Map(operators.map((o) => [o.id.toString(), o.userType]));

    for (const row of rechargeRows) {
      const rowKey = row.id.toString();
      if (row.transactionType === 'PLAYER_DEPOSIT' && row.referenceId) {
        const order = orderByNo.get(row.referenceId);
        result.set(rowKey, {
          depositMethodKey: null,
          depositMethodName: this.paymentMethodDisplayName(order?.paymentMethod),
        });
        continue;
      }

      const operatorType = row.operatorId
        ? operatorTypeById.get(row.operatorId.toString())
        : null;
      let depositMethodKey = 'MANUAL';
      if (operatorType === 'ADMIN') depositMethodKey = 'MANUAL_ADMIN';
      else if (operatorType === 'AGENT') depositMethodKey = 'MANUAL_AGENT';

      result.set(rowKey, { depositMethodKey, depositMethodName: null });
    }

    return result;
  }

  async listWalletTransactionsAdmin(params: {
    page?: number;
    pageSize?: number;
    playerId?: bigint;
    parentAgentId?: bigint;
    parentAgentKeyword?: string;
    scopedParentAgentIds?: bigint[];
    keyword?: string;
    operatorKeyword?: string;
    transactionType?: string;
    typeCategory?: string;
    dateFrom?: Date;
    dateTo?: Date;
  }) {
    const page = Math.max(1, params.page ?? 1);
    const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
    const skip = (page - 1) * pageSize;

    const where: Prisma.WalletTransactionWhereInput = {};

    const explicitType = params.transactionType?.trim();
    if (explicitType) {
      where.transactionType = explicitType;
    } else if (params.typeCategory?.trim()) {
      Object.assign(where, this.walletTypeCategoryWhere(params.typeCategory));
    }

    if (params.dateFrom || params.dateTo) {
      where.createdAt = {};
      if (params.dateFrom) where.createdAt.gte = params.dateFrom;
      if (params.dateTo) where.createdAt.lte = params.dateTo;
    }

    const operatorKeyword = params.operatorKeyword?.trim();
    if (operatorKeyword) {
      const matchedOps = await this.prisma.user.findMany({
        where: {
          deletedAt: null,
          username: { contains: operatorKeyword, mode: 'insensitive' },
        },
        select: { id: true },
        take: 50,
      });
      const operatorIds = matchedOps.map((u) => u.id);
      if (!operatorIds.length) {
        return { items: [], total: 0, page, pageSize };
      }
      where.operatorId = { in: operatorIds };
    }

    let playerIds: bigint[] | undefined;

    if (params.playerId) {
      playerIds = [params.playerId];
    } else {
      const playerWhere: Prisma.UserWhereInput = {
        userType: 'PLAYER',
        deletedAt: null,
      };

      if (params.parentAgentId) {
        playerWhere.parentId = params.parentAgentId;
      } else if (params.parentAgentKeyword?.trim()) {
        const matchedAgents = await this.prisma.user.findMany({
          where: {
            userType: 'AGENT',
            deletedAt: null,
            username: { contains: params.parentAgentKeyword.trim(), mode: 'insensitive' },
            ...(params.scopedParentAgentIds?.length
              ? { id: { in: params.scopedParentAgentIds } }
              : {}),
          },
          select: { id: true },
          take: 50,
        });
        const agentIds = matchedAgents.map((a) => a.id);
        if (!agentIds.length) {
          return { items: [], total: 0, page, pageSize };
        }
        playerWhere.parentId = { in: agentIds };
      } else if (params.scopedParentAgentIds?.length) {
        playerWhere.parentId = { in: params.scopedParentAgentIds };
      }

      const keyword = params.keyword?.trim();
      if (keyword) {
        playerWhere.username = { contains: keyword, mode: 'insensitive' };
      }

      if (
        params.parentAgentId ||
        params.parentAgentKeyword?.trim() ||
        params.scopedParentAgentIds?.length ||
        keyword
      ) {
        const players = await this.prisma.user.findMany({
          where: playerWhere,
          select: { id: true },
          take: 500,
        });
        playerIds = players.map((p) => p.id);
        if (!playerIds.length) {
          return { items: [], total: 0, page, pageSize };
        }
      }
    }

    if (playerIds) {
      where.userId = { in: playerIds };
    }

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

    const userIds = [...new Set(rows.map((r) => r.userId))];
    const operatorIds = [
      ...new Set(rows.map((r) => r.operatorId).filter((id): id is bigint => id != null)),
    ];

    const [players, operators] = await Promise.all([
      userIds.length
        ? this.prisma.user.findMany({
            where: { id: { in: userIds } },
            select: { id: true, username: true, parentId: true },
          })
        : [],
      operatorIds.length
        ? this.prisma.user.findMany({
            where: { id: { in: operatorIds } },
            select: { id: true, username: true },
          })
        : [],
    ]);

    const parentIds = [
      ...new Set(players.map((p) => p.parentId).filter((id): id is bigint => id != null)),
    ];
    const parentAgents = parentIds.length
      ? await this.prisma.user.findMany({
          where: { id: { in: parentIds } },
          select: { id: true, username: true },
        })
      : [];

    const playerById = new Map(players.map((p) => [p.id.toString(), p]));
    const operatorById = new Map(operators.map((u) => [u.id.toString(), u.username]));
    const parentById = new Map(parentAgents.map((a) => [a.id.toString(), a.username]));
    const depositMethodByRowId = await this.resolveDepositMethodsForRows(rows);

    return {
      items: rows.map((row) => {
        const player = playerById.get(row.userId.toString());
        const parentId = player?.parentId;
        const depositMethod = depositMethodByRowId.get(row.id.toString());
        return {
          id: row.id.toString(),
          transactionId: row.transactionId,
          playerId: row.userId.toString(),
          playerUsername: player?.username ?? null,
          parentAgentId: parentId?.toString() ?? null,
          parentAgentUsername: parentId ? (parentById.get(parentId.toString()) ?? null) : null,
          transactionType: row.transactionType,
          amount: row.amount.toString(),
          balanceBefore: row.balanceBefore.toString(),
          balanceAfter: row.balanceAfter.toString(),
          frozenBefore: row.frozenBefore.toString(),
          frozenAfter: row.frozenAfter.toString(),
          referenceType: row.referenceType,
          referenceId: row.referenceId,
          betNo: row.referenceType === 'BET' ? row.referenceId : null,
          depositMethodKey: depositMethod?.depositMethodKey ?? null,
          depositMethodName: depositMethod?.depositMethodName ?? null,
          operatorId: row.operatorId?.toString() ?? null,
          operatorUsername: row.operatorId
            ? (operatorById.get(row.operatorId.toString()) ?? null)
            : null,
          remark: row.remark,
          createdAt: row.createdAt,
        };
      }),
      total,
      page,
      pageSize,
    };
  }

  async listTransferTransactions(params: {
    page?: number;
    pageSize?: number;
    playerId?: bigint;
    parentAgentId?: bigint;
    parentAgentKeyword?: string;
    scopedParentAgentIds?: bigint[];
    keyword?: string;
    operatorKeyword?: string;
    transactionType?: string;
    dateFrom?: Date;
    dateTo?: Date;
  }) {
    const page = Math.max(1, params.page ?? 1);
    const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
    const skip = (page - 1) * pageSize;

    const transferTypes = [
      'MANUAL_DEPOSIT',
      'MANUAL_WITHDRAW',
      'ADMIN_DEPOSIT',
      'ADMIN_WITHDRAW',
      'AGENT_DEPOSIT',
      'AGENT_WITHDRAW',
      'INITIAL_DEPOSIT',
      'PLAYER_DEPOSIT',
    ];
    const where: Prisma.WalletTransactionWhereInput = {
      transactionType: params.transactionType?.trim()
        ? params.transactionType.trim()
        : { in: transferTypes },
    };

    if (params.dateFrom || params.dateTo) {
      where.createdAt = {};
      if (params.dateFrom) where.createdAt.gte = params.dateFrom;
      if (params.dateTo) where.createdAt.lte = params.dateTo;
    }

    const operatorKeyword = params.operatorKeyword?.trim();
    if (operatorKeyword) {
      const matchedOps = await this.prisma.user.findMany({
        where: {
          deletedAt: null,
          username: { contains: operatorKeyword, mode: 'insensitive' },
        },
        select: { id: true },
        take: 50,
      });
      const operatorIds = matchedOps.map((u) => u.id);
      if (!operatorIds.length) {
        return { items: [], total: 0, page, pageSize };
      }
      where.operatorId = { in: operatorIds };
    }

    let playerIds: bigint[] | undefined;

    if (params.playerId) {
      playerIds = [params.playerId];
    } else {
      const playerWhere: Prisma.UserWhereInput = {
        userType: 'PLAYER',
        deletedAt: null,
      };

      if (params.parentAgentId) {
        playerWhere.parentId = params.parentAgentId;
      } else if (params.parentAgentKeyword?.trim()) {
        const matchedAgents = await this.prisma.user.findMany({
          where: {
            userType: 'AGENT',
            deletedAt: null,
            username: { contains: params.parentAgentKeyword.trim(), mode: 'insensitive' },
            ...(params.scopedParentAgentIds?.length
              ? { id: { in: params.scopedParentAgentIds } }
              : {}),
          },
          select: { id: true },
          take: 50,
        });
        const agentIds = matchedAgents.map((a) => a.id);
        if (!agentIds.length) {
          return { items: [], total: 0, page, pageSize };
        }
        playerWhere.parentId = { in: agentIds };
      } else if (params.scopedParentAgentIds?.length) {
        playerWhere.parentId = { in: params.scopedParentAgentIds };
      }

      const keyword = params.keyword?.trim();
      if (keyword) {
        playerWhere.username = { contains: keyword, mode: 'insensitive' };
      }

      if (params.parentAgentId || params.parentAgentKeyword?.trim() || params.scopedParentAgentIds?.length || keyword) {
        const players = await this.prisma.user.findMany({
          where: playerWhere,
          select: { id: true },
          take: 500,
        });
        playerIds = players.map((p) => p.id);
        if (!playerIds.length) {
          return { items: [], total: 0, page, pageSize };
        }
      }
    }

    if (playerIds) {
      where.userId = { in: playerIds };
    }

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

    const userIds = [...new Set(rows.map((r) => r.userId))];
    const operatorIds = [
      ...new Set(rows.map((r) => r.operatorId).filter((id): id is bigint => id != null)),
    ];

    const [players, operators] = await Promise.all([
      userIds.length
        ? this.prisma.user.findMany({
            where: { id: { in: userIds } },
            select: { id: true, username: true, parentId: true },
          })
        : [],
      operatorIds.length
        ? this.prisma.user.findMany({
            where: { id: { in: operatorIds } },
            select: { id: true, username: true },
          })
        : [],
    ]);

    const parentIds = [
      ...new Set(players.map((p) => p.parentId).filter((id): id is bigint => id != null)),
    ];
    const parentAgents = parentIds.length
      ? await this.prisma.user.findMany({
          where: { id: { in: parentIds } },
          select: { id: true, username: true },
        })
      : [];

    const playerById = new Map(players.map((p) => [p.id.toString(), p]));
    const operatorById = new Map(operators.map((u) => [u.id.toString(), u.username]));
    const parentById = new Map(parentAgents.map((a) => [a.id.toString(), a.username]));

    return {
      items: rows.map((row) => {
        const player = playerById.get(row.userId.toString());
        const parentId = player?.parentId;
        return {
          id: row.id.toString(),
          transactionId: row.transactionId,
          playerId: row.userId.toString(),
          playerUsername: player?.username ?? null,
          parentAgentId: parentId?.toString() ?? null,
          parentAgentUsername: parentId ? (parentById.get(parentId.toString()) ?? null) : null,
          transactionType: row.transactionType,
          amount: row.amount.toString(),
          balanceBefore: row.balanceBefore.toString(),
          balanceAfter: row.balanceAfter.toString(),
          frozenBefore: row.frozenBefore.toString(),
          frozenAfter: row.frozenAfter.toString(),
          operatorId: row.operatorId?.toString() ?? null,
          operatorUsername: row.operatorId
            ? (operatorById.get(row.operatorId.toString()) ?? null)
            : null,
          remark: row.remark,
          createdAt: row.createdAt,
        };
      }),
      total,
      page,
      pageSize,
    };
  }

  async getTransactionStats(userId: bigint) {
    const [aggregates, byType] = await Promise.all([
      this.prisma.walletTransaction.aggregate({
        where: { userId },
        _sum: { amount: true },
        _count: { id: true },
      }),
      this.prisma.walletTransaction.groupBy({
        by: ['transactionType'],
        where: { userId },
        _count: { id: true },
        _sum: { amount: true },
      }),
    ]);

    return {
      total: aggregates._count.id,
      netAmount: aggregates._sum.amount?.toString() ?? '0',
      byType: byType.map((g) => ({
        transactionType: g.transactionType,
        count: g._count.id,
        totalAmount: g._sum.amount?.toString() ?? '0',
      })),
    };
  }
}
