import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Decimal } from '@prisma/client/runtime/library';
import { resolveTranslationFallback } from '@thebet365/shared';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { FundsPostingService } from '../ledger/funds-posting.service';
import { AgentCreditService } from '../agent/agent-credit.service';
import { appBadRequest } from '../../shared/common/app-error';
import { deleteUploadFileByUrl } from '../../shared/uploads/delete-upload-file';
import { PlayerMessagesService } from '../player-messages/player-messages.service';
import { SystemConfigService } from '../../shared/config/system-config.service';

function generateOrderNo(): string {
  const ts = Date.now().toString(36).toUpperCase();
  const rand = Math.random().toString(36).substring(2, 8).toUpperCase();
  return `DEP${ts}${rand}`;
}

/** 已通过充值订单允许撤回的时间窗口 */
const DEPOSIT_REVOKE_WINDOW_MS = 5 * 60 * 1000;

export type DepositOrderAuditAction =
  | 'SUBMITTED'
  | 'APPROVED'
  | 'REJECTED'
  | 'REVOKED'
  | 'REOPENED'
  | 'DELETED';

type AuditLogWriter = {
  depositOrderAuditLog: {
    create: (args: {
      data: {
        depositOrderId: bigint;
        action: DepositOrderAuditAction;
        actorId?: bigint | null;
        actorType: string;
        statusBefore?: string | null;
        statusAfter: string;
        amount?: Decimal | null;
        approvedAmount?: Decimal | null;
        remark?: string | null;
      };
    }) => Promise<{ id: bigint }>;
  };
};

@Injectable()
export class DepositService {
  constructor(
    private prisma: PrismaService,
    private funds: FundsPostingService,
    private credit: AgentCreditService,
    private playerMessages: PlayerMessagesService,
    private systemConfig: SystemConfigService,
  ) {}

  // ============ Payment Methods (Admin CRUD) ============

  /** isActive 与 showOnPlayer 合并为同一开关，DB 两列保持同步以兼容旧数据。 */
  private normalizePaymentMethodActive(data: {
    isActive?: boolean;
    showOnPlayer?: boolean;
  }): { isActive?: boolean; showOnPlayer?: boolean } {
    if (data.isActive !== undefined) {
      return { isActive: data.isActive, showOnPlayer: data.isActive };
    }
    if (data.showOnPlayer !== undefined) {
      return { isActive: data.showOnPlayer, showOnPlayer: data.showOnPlayer };
    }
    return {};
  }

  async createPaymentMethod(data: {
    methodType: string;
    bankName?: string;
    accountHolder?: string;
    accountNumber?: string;
    usdtAddress?: string;
    qrCodeUrl?: string;
    displayName?: string;
    sortOrder?: number;
    isActive?: boolean;
    showOnPlayer?: boolean;
    createdBy?: bigint;
    translations?: {
      displayName?: Record<string, string>;
      bankName?: Record<string, string>;
    };
  }) {
    const active = data.isActive ?? data.showOnPlayer ?? true;
    const method = await this.prisma.paymentMethod.create({
      data: {
        methodType: data.methodType,
        bankName: data.bankName,
        accountHolder: data.accountHolder,
        accountNumber: data.accountNumber,
        usdtAddress: data.usdtAddress,
        qrCodeUrl: data.qrCodeUrl,
        displayName: data.displayName,
        sortOrder: data.sortOrder ?? 0,
        isActive: active,
        showOnPlayer: active,
        createdBy: data.createdBy,
      },
    });
    if (data.translations) {
      await this.upsertPaymentMethodTranslations(method.id, data.translations);
    }
    return method;
  }

  async updatePaymentMethod(
    id: bigint,
    data: {
      bankName?: string;
      accountHolder?: string;
      accountNumber?: string;
      usdtAddress?: string;
      qrCodeUrl?: string;
      displayName?: string;
      sortOrder?: number;
      isActive?: boolean;
      showOnPlayer?: boolean;
      translations?: {
        displayName?: Record<string, string>;
        bankName?: Record<string, string>;
      };
    },
  ) {
    const { translations, ...rest } = data;
    const activePatch = this.normalizePaymentMethodActive(rest);
    const method = await this.prisma.paymentMethod.update({
      where: { id },
      data: { ...rest, ...activePatch },
    });
    if (translations) {
      await this.upsertPaymentMethodTranslations(id, translations);
    }
    return method;
  }

  async deletePaymentMethod(id: bigint) {
    return this.prisma.paymentMethod.update({
      where: { id },
      data: { isActive: false, showOnPlayer: false },
    });
  }

  async listPaymentMethods(filters?: { methodType?: string }) {
    const where: Prisma.PaymentMethodWhereInput = {};
    if (filters?.methodType) {
      where.methodType = filters.methodType;
    }
    const items = await this.prisma.paymentMethod.findMany({
      where,
      orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
    });
    // Attach translations for admin editing
    const ids = items.map((m) => m.id);
    const translations = ids.length
      ? await this.prisma.entityTranslation.findMany({
          where: { entityType: 'PAYMENT_METHOD', entityId: { in: ids } },
        })
      : [];
    const tMap = new Map<string, Record<string, Record<string, string>>>();
    for (const t of translations) {
      const key = t.entityId.toString();
      if (!tMap.has(key)) tMap.set(key, {});
      const entityMap = tMap.get(key)!;
      if (!entityMap[t.fieldName]) entityMap[t.fieldName] = {};
      entityMap[t.fieldName][t.locale] = t.value;
    }
    return items.map((m) => ({
      ...m,
      translations: tMap.get(m.id.toString()) ?? {},
    }));
  }

  async listPlayerPaymentMethods(methodType?: string, locale?: string) {
    const where: Prisma.PaymentMethodWhereInput = {
      isActive: true,
    };
    if (methodType) {
      where.methodType = methodType;
    }
    const items = await this.prisma.paymentMethod.findMany({
      where,
      orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
      select: {
        id: true,
        methodType: true,
        bankName: true,
        accountHolder: true,
        accountNumber: true,
        usdtAddress: true,
        qrCodeUrl: true,
        displayName: true,
        sortOrder: true,
      },
    });
    // Resolve translations for player locale
    if (locale && items.length) {
      const ids = items.map((m) => m.id);
      const translations = await this.prisma.entityTranslation.findMany({
        where: { entityType: 'PAYMENT_METHOD', entityId: { in: ids } },
      });
      const tMap = new Map<string, Record<string, Record<string, string>>>();
      for (const t of translations) {
        const key = t.entityId.toString();
        if (!tMap.has(key)) tMap.set(key, {});
        const entityMap = tMap.get(key)!;
        if (!entityMap[t.fieldName]) entityMap[t.fieldName] = {};
        entityMap[t.fieldName][t.locale] = t.value;
      }
      return items.map((m) => {
        const t = tMap.get(m.id.toString());
        const resolvedDisplayName = t?.displayName
          ? resolveTranslationFallback(t.displayName, locale) || m.displayName
          : m.displayName;
        const resolvedBankName = t?.bankName
          ? resolveTranslationFallback(t.bankName, locale) || m.bankName
          : m.bankName;
        return { ...m, displayName: resolvedDisplayName, bankName: resolvedBankName };
      });
    }
    return items;
  }

  // ============ Translation helpers ============

  private async upsertPaymentMethodTranslations(
    entityId: bigint,
    translations: {
      displayName?: Record<string, string>;
      bankName?: Record<string, string>;
    },
  ) {
    for (const fieldName of ['displayName', 'bankName'] as const) {
      const fieldTranslations = translations[fieldName];
      if (!fieldTranslations) continue;
      for (const [locale, value] of Object.entries(fieldTranslations)) {
        await this.prisma.entityTranslation.upsert({
          where: {
            entityType_entityId_locale_fieldName: {
              entityType: 'PAYMENT_METHOD',
              entityId,
              locale,
              fieldName,
            },
          },
          create: { entityType: 'PAYMENT_METHOD', entityId, locale, fieldName, value },
          update: { value },
        });
      }
    }
  }

  // ============ Deposit Orders ============

  private async getPlayerLocale(playerId: bigint, tx: Prisma.TransactionClient | PrismaService = this.prisma) {
    const user = await tx.user.findUnique({
      where: { id: playerId },
      select: { locale: true },
    });
    return user?.locale ?? 'en-US';
  }

  private async recordDepositAudit(
    client: AuditLogWriter,
    data: {
      depositOrderId: bigint;
      action: DepositOrderAuditAction;
      actorId?: bigint | null;
      actorType: 'PLAYER' | 'ADMIN';
      statusBefore?: string | null;
      statusAfter: string;
      amount?: Decimal | null;
      approvedAmount?: Decimal | null;
      remark?: string | null;
    },
  ) {
    return client.depositOrderAuditLog.create({
      data: {
        depositOrderId: data.depositOrderId,
        action: data.action,
        actorId: data.actorId ?? null,
        actorType: data.actorType,
        statusBefore: data.statusBefore ?? null,
        statusAfter: data.statusAfter,
        amount: data.amount ?? null,
        approvedAmount: data.approvedAmount ?? null,
        remark: data.remark ?? null,
      },
    });
  }

  private mapAuditLogRow(
    row: {
      id: bigint;
      action: string;
      actorId: bigint | null;
      actorType: string;
      statusBefore: string | null;
      statusAfter: string;
      amount: Decimal | null;
      approvedAmount: Decimal | null;
      remark: string | null;
      createdAt: Date;
    },
    actorUsername?: string | null,
  ) {
    return {
      id: row.id.toString(),
      action: row.action,
      actorId: row.actorId?.toString() ?? null,
      actorType: row.actorType,
      actorUsername: actorUsername ?? null,
      statusBefore: row.statusBefore,
      statusAfter: row.statusAfter,
      amount: row.amount?.toString() ?? null,
      approvedAmount: row.approvedAmount?.toString() ?? null,
      remark: row.remark,
      createdAt: row.createdAt.toISOString(),
    };
  }

  async getDepositOrderAuditLogs(orderId: bigint) {
    const order = await this.prisma.depositOrder.findUnique({
      where: { id: orderId },
      select: { id: true },
    });
    if (!order) throw appBadRequest('ORDER_NOT_FOUND');

    const rows = await this.prisma.depositOrderAuditLog.findMany({
      where: { depositOrderId: orderId },
      orderBy: { createdAt: 'asc' },
    });

    const actorIds = [...new Set(rows.map((r) => r.actorId).filter((id): id is bigint => id != null))];
    const actors =
      actorIds.length > 0
        ? await this.prisma.user.findMany({
            where: { id: { in: actorIds } },
            select: { id: true, username: true },
          })
        : [];
    const actorMap = new Map(actors.map((a) => [a.id.toString(), a.username]));

    return rows.map((row) =>
      this.mapAuditLogRow(
        row,
        row.actorId ? actorMap.get(row.actorId.toString()) ?? null : null,
      ),
    );
  }

  async getPlayerDepositOrderAuditLogs(orderId: bigint, playerId: bigint) {
    const order = await this.prisma.depositOrder.findFirst({
      where: { id: orderId, playerId },
      select: { id: true },
    });
    if (!order) throw appBadRequest('ORDER_NOT_FOUND');

    const rows = await this.prisma.depositOrderAuditLog.findMany({
      where: { depositOrderId: orderId },
      orderBy: { createdAt: 'asc' },
    });

    return rows.map((row) => {
      const mapped = this.mapAuditLogRow(row);
      return {
        id: mapped.id,
        action: mapped.action,
        actorType: mapped.actorType,
        statusBefore: mapped.statusBefore,
        statusAfter: mapped.statusAfter,
        amount: mapped.amount,
        approvedAmount: mapped.approvedAmount,
        remark: mapped.remark,
        createdAt: mapped.createdAt,
      };
    });
  }

  private async attachPlayerAuditLogs(
    orders: Array<{ id: bigint }>,
  ) {
    if (!orders.length) return new Map<string, ReturnType<typeof this.mapAuditLogRow>[]>();

    const orderIds = orders.map((o) => o.id);
    const rows = await this.prisma.depositOrderAuditLog.findMany({
      where: { depositOrderId: { in: orderIds } },
      orderBy: { createdAt: 'asc' },
    });

    const map = new Map<string, ReturnType<typeof this.mapAuditLogRow>[]>();
    for (const row of rows) {
      const key = row.depositOrderId.toString();
      if (!map.has(key)) map.set(key, []);
      const mapped = this.mapAuditLogRow(row);
      map.get(key)!.push({
        id: mapped.id,
        action: mapped.action,
        actorId: null,
        actorType: mapped.actorType,
        actorUsername: null,
        statusBefore: mapped.statusBefore,
        statusAfter: mapped.statusAfter,
        amount: mapped.amount,
        approvedAmount: mapped.approvedAmount,
        remark: mapped.remark,
        createdAt: mapped.createdAt,
      });
    }
    return map;
  }

  async createDepositOrder(
    playerId: bigint,
    paymentMethodId: bigint,
    amount: number,
    screenshotUrl: string,
  ) {
    const method = await this.prisma.paymentMethod.findUnique({
      where: { id: paymentMethodId },
    });
    if (!method || !method.isActive) {
      throw appBadRequest('PAYMENT_METHOD_NOT_FOUND');
    }

    const order = await this.prisma.depositOrder.create({
      data: {
        orderNo: generateOrderNo(),
        playerId,
        paymentMethodId,
        methodType: method.methodType,
        amount: new Decimal(amount),
        screenshotUrl,
        status: 'PENDING',
      },
    });

    await this.recordDepositAudit(this.prisma, {
      depositOrderId: order.id,
      action: 'SUBMITTED',
      actorId: playerId,
      actorType: 'PLAYER',
      statusBefore: null,
      statusAfter: 'PENDING',
      amount: order.amount,
      remark: null,
    });

    return order;
  }

  /** 玩家对已拒绝订单重新提交：原订单号不变，状态恢复待审核，需新转账截图。 */
  async reapplyDepositOrder(
    playerId: bigint,
    orderId: bigint,
    screenshotUrl: string,
    amount?: number,
    paymentMethodId?: bigint,
  ) {
    return this.prisma.$transaction(async (tx) => {
      const order = await this.lockDepositOrder(tx, orderId);
      if (order.playerId !== playerId) throw appBadRequest('ORDER_NOT_FOUND');
      if (order.status !== 'REJECTED') throw appBadRequest('ORDER_NOT_REJECTED');

      const otherPending = await tx.depositOrder.findFirst({
        where: {
          playerId,
          status: 'PENDING',
          id: { not: orderId },
        },
        select: { id: true },
      });
      if (otherPending) throw appBadRequest('DEPOSIT_PENDING_ORDER_EXISTS');

      const targetMethodId = paymentMethodId ?? order.paymentMethodId;
      const method = await tx.paymentMethod.findUnique({ where: { id: targetMethodId } });
      if (!method || !method.isActive) {
        throw appBadRequest('PAYMENT_METHOD_NOT_FOUND');
      }

      const creditAmount = amount != null ? new Decimal(amount) : order.amount;
      if (creditAmount.lte(0)) throw appBadRequest('INVALID_AMOUNT');

      const oldScreenshotUrl = order.screenshotUrl;

      await tx.depositOrder.update({
        where: { id: orderId },
        data: {
          status: 'PENDING',
          screenshotUrl,
          amount: creditAmount,
          paymentMethodId: targetMethodId,
          methodType: method.methodType,
          approvedAmount: null,
          reviewerId: null,
          reviewedAt: null,
          rejectReason: null,
          remark: null,
        },
      });

      await this.recordDepositAudit(tx, {
        depositOrderId: orderId,
        action: 'REOPENED',
        actorId: playerId,
        actorType: 'PLAYER',
        statusBefore: order.status,
        statusAfter: 'PENDING',
        amount: creditAmount,
        remark: null,
      });

      await deleteUploadFileByUrl(oldScreenshotUrl);

      return tx.depositOrder.findUnique({ where: { id: orderId } });
    });
  }

  async getPlayerDepositOrders(playerId: bigint, page = 1, pageSize = 20) {
    const skip = (page - 1) * pageSize;
    const where = { playerId };

    const [items, total] = await Promise.all([
      this.prisma.depositOrder.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: pageSize,
        include: {
          paymentMethod: {
            select: { bankName: true, usdtAddress: true, displayName: true, methodType: true },
          },
        },
      }),
      this.prisma.depositOrder.count({ where }),
    ]);
    const auditMap = await this.attachPlayerAuditLogs(items);

    return {
      items: items.map((o) => ({
        id: o.id.toString(),
        orderNo: o.orderNo,
        paymentMethodId: o.paymentMethodId.toString(),
        methodType: o.methodType,
        amount: o.amount.toString(),
        screenshotUrl: o.screenshotUrl,
        status: o.status,
        approvedAmount: o.approvedAmount?.toString() ?? null,
        rejectReason: o.rejectReason,
        remark: o.remark,
        createdAt: o.createdAt,
        reviewedAt: o.reviewedAt,
        paymentMethodName: o.paymentMethod?.displayName ?? o.paymentMethod?.bankName ?? o.paymentMethod?.usdtAddress ?? null,
        auditLogs: (auditMap.get(o.id.toString()) ?? []).map((log) => ({
          id: log.id,
          action: log.action,
          actorType: log.actorType,
          statusBefore: log.statusBefore,
          statusAfter: log.statusAfter,
          amount: log.amount,
          approvedAmount: log.approvedAmount,
          remark: log.remark,
          createdAt: log.createdAt,
        })),
      })),
      total,
      page,
      pageSize,
    };
  }

  async listDepositOrders(params: {
    page?: number;
    pageSize?: number;
    status?: string;
    keyword?: string;
    methodType?: 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.DepositOrderWhereInput = {};

    if (params.status) {
      where.status = params.status;
    }

    if (params.methodType) {
      where.methodType = params.methodType;
    }

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

    if (params.keyword?.trim()) {
      const players = await this.prisma.user.findMany({
        where: {
          userType: 'PLAYER',
          deletedAt: null,
          username: { contains: params.keyword.trim(), mode: 'insensitive' },
        },
        select: { id: true },
        take: 100,
      });
      if (!players.length) {
        return { items: [], total: 0, page, pageSize };
      }
      where.playerId = { in: players.map((p) => p.id) };
    }

    const [rows, total] = await Promise.all([
      this.prisma.depositOrder.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip,
        take: pageSize,
        include: {
          paymentMethod: {
            select: { bankName: true, usdtAddress: true, displayName: true, methodType: true },
          },
        },
      }),
      this.prisma.depositOrder.count({ where }),
    ]);

    // Enrich with player usernames and reviewer info
    const playerIds = [...new Set(rows.map((r) => r.playerId))];
    const reviewerIds = [...new Set(rows.map((r) => r.reviewerId).filter((id): id is bigint => id != null))];

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

    const playerMap = new Map(players.map((p) => [p.id.toString(), p.username]));
    const reviewerMap = new Map(reviewers.map((r) => [r.id.toString(), r.username]));

    return {
      items: rows.map((o) => ({
        id: o.id.toString(),
        orderNo: o.orderNo,
        playerId: o.playerId.toString(),
        playerUsername: playerMap.get(o.playerId.toString()) ?? null,
        methodType: o.methodType,
        amount: o.amount.toString(),
        screenshotUrl: o.screenshotUrl,
        status: o.status,
        approvedAmount: o.approvedAmount?.toString() ?? null,
        reviewerId: o.reviewerId?.toString() ?? null,
        reviewerUsername: o.reviewerId ? (reviewerMap.get(o.reviewerId.toString()) ?? null) : null,
        rejectReason: o.rejectReason,
        remark: o.remark,
        createdAt: o.createdAt,
        reviewedAt: o.reviewedAt,
        paymentMethodName: o.paymentMethod?.displayName ?? o.paymentMethod?.bankName ?? o.paymentMethod?.usdtAddress ?? null,
      })),
      total,
      page,
      pageSize,
    };
  }

  async countPendingDepositOrders(): Promise<number> {
    return this.prisma.depositOrder.count({ where: { status: 'PENDING' } });
  }

  async approveDepositOrder(
    orderId: bigint,
    operatorId: bigint,
    approvedAmount?: number,
    remark?: string,
  ) {
    return this.prisma.$transaction(async (tx) => {
      const order = await tx.depositOrder.findUnique({ where: { id: orderId } });
      if (!order) throw appBadRequest('ORDER_NOT_FOUND');
      if (order.status !== 'PENDING') throw appBadRequest('ORDER_NOT_PENDING');

      const creditAmount = approvedAmount != null ? new Decimal(approvedAmount) : order.amount;
      const parentAgentId = await this.assertParentCreditForDeposit(tx, order.playerId, creditAmount);

      await tx.depositOrder.update({
        where: { id: orderId },
        data: {
          status: 'APPROVED',
          approvedAmount: creditAmount,
          reviewerId: operatorId,
          reviewedAt: new Date(),
          remark: remark ?? null,
        },
      });

      const auditLog = await this.recordDepositAudit(tx, {
        depositOrderId: orderId,
        action: 'APPROVED',
        actorId: operatorId,
        actorType: 'ADMIN',
        statusBefore: order.status,
        statusAfter: 'APPROVED',
        amount: order.amount,
        approvedAmount: creditAmount,
        remark: remark ?? null,
      });

      // Credit player wallet
      await this.funds.deposit({
        userId: order.playerId,
        amount: creditAmount,
        operatorId,
        remark: remark ?? `Deposit order ${order.orderNo}`,
        referenceId: order.orderNo,
        transactionType: 'PLAYER_DEPOSIT',
        tx,
        businessKey: `deposit:${order.orderNo}:approve:${auditLog.id}`,
      });
      if (parentAgentId) {
        await this.credit.recalculateUsedCredit(parentAgentId, tx);
      }

      const playerLocale = await this.getPlayerLocale(order.playerId, tx);
      const inboxNotify = await this.systemConfig.getInboxNotifySettings();
      if (inboxNotify.inboxEnabled && inboxNotify.deposit) {
        await this.playerMessages.createDepositApprovedMessage(
          order.playerId,
          {
            depositOrderId: orderId,
            orderNo: order.orderNo,
            amount: order.amount.toString(),
            approvedAmount: creditAmount.toString(),
            locale: playerLocale,
          },
          tx,
        );
      }

      return { success: true };
    });
  }

  async rejectDepositOrder(orderId: bigint, operatorId: bigint, reason: string) {
    const order = await this.prisma.depositOrder.findUnique({ where: { id: orderId } });
    if (!order) throw appBadRequest('ORDER_NOT_FOUND');
    if (order.status !== 'PENDING') throw appBadRequest('ORDER_NOT_PENDING');

    await this.prisma.depositOrder.update({
      where: { id: orderId },
      data: {
        status: 'REJECTED',
        reviewerId: operatorId,
        reviewedAt: new Date(),
        rejectReason: reason,
        remark: reason,
      },
    });

    await this.recordDepositAudit(this.prisma, {
      depositOrderId: orderId,
      action: 'REJECTED',
      actorId: operatorId,
      actorType: 'ADMIN',
      statusBefore: order.status,
      statusAfter: 'REJECTED',
      amount: order.amount,
      remark: reason,
    });

    const playerLocale = await this.getPlayerLocale(order.playerId);
    const inboxNotify = await this.systemConfig.getInboxNotifySettings();
    if (inboxNotify.inboxEnabled && inboxNotify.deposit) {
      await this.playerMessages.createDepositRejectedMessage(order.playerId, {
        depositOrderId: orderId,
        orderNo: order.orderNo,
        amount: order.amount.toString(),
        rejectReason: reason,
        locale: playerLocale,
      });
    }

    return { success: true };
  }

  private async reverseApprovedDepositCredit(
    order: {
      playerId: bigint;
      orderNo: string;
      approvedAmount: Decimal | null;
      amount: Decimal;
    },
    operatorId: bigint,
    remark: string,
  ) {
    const credit = order.approvedAmount ?? order.amount;
    await this.funds.withdraw({
      userId: order.playerId,
      amount: credit,
      operatorId,
      remark,
      referenceId: order.orderNo,
      transactionType: 'PLAYER_DEPOSIT_REVERSAL',
      businessKey: `deposit:${order.orderNo}:reverse`,
    });
  }

  /** 已拒绝：恢复待审核；已通过（5 分钟内且未产生下注）：扣回入账并恢复待审核 */
  async reopenDepositOrderForReview(orderId: bigint, operatorId: bigint) {
    return this.prisma.$transaction(async (tx) => {
      const order = await this.lockDepositOrder(tx, orderId);
      if (order.status === 'PENDING') throw appBadRequest('ORDER_ALREADY_PENDING');

      if (order.status === 'REJECTED') {
        await tx.depositOrder.update({
          where: { id: orderId },
          data: {
            status: 'PENDING',
            approvedAmount: null,
            reviewerId: null,
            reviewedAt: null,
            rejectReason: null,
            remark: null,
          },
        });
        await this.recordDepositAudit(tx, {
          depositOrderId: orderId,
          action: 'REOPENED',
          actorId: operatorId,
          actorType: 'ADMIN',
          statusBefore: order.status,
          statusAfter: 'PENDING',
          amount: order.amount,
          remark: null,
        });
        return { success: true };
      }

      if (order.status !== 'APPROVED') {
        throw appBadRequest('ORDER_NOT_APPROVED');
      }

      if (!order.reviewedAt || Date.now() - order.reviewedAt.getTime() > DEPOSIT_REVOKE_WINDOW_MS) {
        throw appBadRequest('DEPOSIT_REVOKE_WINDOW_EXPIRED');
      }

      await this.lockPlayerWallet(tx, order.playerId);
      const reviewedAt = order.reviewedAt;
      const betsAfterReview = await tx.bet.findMany({
        where: {
          userId: order.playerId,
          placedAt: { gte: reviewedAt },
          status: { not: 'VOID' },
        },
        select: { id: true },
      });

      if (betsAfterReview.length > 0) {
        throw appBadRequest('DEPOSIT_REVOKE_SETTLED_BETS');
      }

      const credit = order.approvedAmount ?? order.amount;
      const parentAgentId = await this.findPlayerParentAgentId(tx, order.playerId);
      const approvalCycleKey = order.reviewedAt?.getTime();
      await this.funds.withdraw({
        userId: order.playerId,
        amount: credit,
        operatorId,
        remark: `Revoke approved deposit ${order.orderNo}`,
        referenceId: order.orderNo,
        transactionType: 'PLAYER_DEPOSIT_REVERSAL',
        referenceType: 'DEPOSIT',
        tx,
        businessKey: `deposit:${order.orderNo}:reopen-reverse:${approvalCycleKey}`,
      });
      if (parentAgentId) {
        await this.credit.recalculateUsedCredit(parentAgentId, tx);
      }

      await tx.depositOrder.update({
        where: { id: orderId },
        data: {
          status: 'PENDING',
          approvedAmount: null,
          reviewerId: null,
          reviewedAt: null,
          rejectReason: null,
          remark: null,
        },
      });

      await this.recordDepositAudit(tx, {
        depositOrderId: orderId,
        action: 'REVOKED',
        actorId: operatorId,
        actorType: 'ADMIN',
        statusBefore: order.status,
        statusAfter: 'PENDING',
        amount: order.amount,
        approvedAmount: credit,
        remark: `Revoke approved deposit ${order.orderNo}`,
      });

      return { success: true, voidedBets: 0 };
    });
  }

  /** 删除充值订单记录及截图，仅允许删除未资金化订单。 */
  async deleteDepositOrder(orderId: bigint, _operatorId: bigint) {
    const order = await this.prisma.depositOrder.findUnique({ where: { id: orderId } });
    if (!order) throw appBadRequest('ORDER_NOT_FOUND');
    if (order.status === 'APPROVED') {
      throw appBadRequest('DEPOSIT_ORDER_FUNDED_DELETE_FORBIDDEN');
    }

    const screenshotUrl = order.screenshotUrl;
    await this.prisma.depositOrder.delete({ where: { id: orderId } });
    await deleteUploadFileByUrl(screenshotUrl);

    return { success: true };
  }

  private async lockDepositOrder(tx: Prisma.TransactionClient, orderId: bigint) {
    const rows = await tx.$queryRaw<Array<{ id: bigint }>>`
      SELECT id FROM deposit_orders WHERE id = ${orderId} FOR UPDATE
    `;
    if (!rows.length) throw appBadRequest('ORDER_NOT_FOUND');
    const order = await tx.depositOrder.findUnique({ where: { id: orderId } });
    if (!order) throw appBadRequest('ORDER_NOT_FOUND');
    return order;
  }

  private async lockPlayerWallet(tx: Prisma.TransactionClient, playerId: bigint) {
    const rows = await tx.$queryRaw<Array<{ id: bigint }>>`
      SELECT id FROM wallets WHERE user_id = ${playerId} FOR UPDATE
    `;
    if (!rows.length) throw appBadRequest('WALLET_NOT_FOUND');
  }

  private async findPlayerParentAgentId(tx: Prisma.TransactionClient, playerId: bigint) {
    const player = await tx.user.findFirst({
      where: { id: playerId, userType: 'PLAYER', deletedAt: null },
      select: { parentId: true },
    });
    return player?.parentId ?? null;
  }

  private async assertParentCreditForDeposit(
    tx: Prisma.TransactionClient,
    playerId: bigint,
    amount: Decimal,
  ) {
    const parentAgentId = await this.findPlayerParentAgentId(tx, playerId);
    if (!parentAgentId) return null;

    await this.credit.recalculateUsedCredit(parentAgentId, tx);
    const profile = await tx.agentProfile.findUnique({
      where: { userId: parentAgentId },
      select: { creditLimit: true, usedCredit: true },
    });
    if (!profile) throw appBadRequest('AGENT_PROFILE_NOT_FOUND');

    const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
    if (available.lt(amount)) {
      throw appBadRequest('CREDIT_TOPUP_EXCEEDED');
    }
    return parentAgentId;
  }
}
