import {
  Injectable,
  BadRequestException,
  ForbiddenException,
  NotFoundException,
} from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { WalletService } from '../ledger/wallet.service';
import { AuthService } from '../identity/auth.service';
import { SystemConfigService } from '../../shared/config/system-config.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../shared/common/decorators';
import { assertPlayerUsername } from '@thebet365/shared';

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

function sub(a: Decimal | null | undefined, b: Decimal | null | undefined) {
  return new Decimal(a ?? 0).sub(b ?? 0).toString();
}

@Injectable()
export class AgentsService {
  constructor(
    private prisma: PrismaService,
    private wallet: WalletService,
    private auth: AuthService,
    private systemConfig: SystemConfigService,
  ) {}

  async getProfile(agentId: bigint) {
    const profile = await this.prisma.agentProfile.findUnique({
      where: { userId: agentId },
    });
    if (!profile) throw new BadRequestException('Agent profile not found');
    const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
    return { ...profile, availableCredit: available };
  }

  async recalculateUsedCredit(agentId: bigint) {
    const directPlayers = await this.prisma.user.findMany({
      where: { parentId: agentId, userType: 'PLAYER' },
      include: { wallet: true },
    });

    let directLiability = new Decimal(0);
    for (const p of directPlayers) {
      if (p.wallet) {
        directLiability = directLiability
          .add(p.wallet.availableBalance)
          .add(p.wallet.frozenBalance);
      }
    }

    const childAgents = await this.prisma.agentProfile.findMany({
      where: { parentAgentId: agentId },
    });

    let childExposure = new Decimal(0);
    for (const child of childAgents) {
      const exposure = Decimal.max(child.creditLimit, child.usedCredit);
      childExposure = childExposure.add(exposure);
    }

    const usedCredit = directLiability.add(childExposure);

    await this.prisma.agentProfile.update({
      where: { userId: agentId },
      data: {
        usedCredit,
        directPlayerLiability: directLiability,
        childAgentExposure: childExposure,
      },
    });

    return usedCredit;
  }

  async adjustCredit(
    agentId: bigint,
    amount: Decimal | number,
    operatorId: bigint,
    requestId: string,
    remark?: string,
  ) {
    const amt = new Decimal(amount);
    const profile = await this.prisma.agentProfile.findUnique({
      where: { userId: agentId },
    });
    if (!profile) throw new BadRequestException('Agent not found');

    const creditBefore = profile.creditLimit;
    const creditAfter = creditBefore.add(amt);
    if (creditAfter.lt(0)) throw new BadRequestException('Credit limit cannot be negative');

    if (profile.parentAgentId) {
      await this.assertChildCreditWithinParent(profile.parentAgentId, profile, creditAfter);
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.agentProfile.update({
        where: { userId: agentId },
        data: { creditLimit: creditAfter },
      });

      await tx.agentCreditTransaction.create({
        data: {
          agentId,
          transactionType: amt.gte(0) ? 'CREDIT_INCREASE' : 'CREDIT_DECREASE',
          amount: amt,
          creditBefore,
          creditAfter,
          operatorId,
          requestId,
          remark,
        },
      });
    });

    if (profile.parentAgentId) {
      await this.recalculateUsedCredit(profile.parentAgentId);
    }

    return { creditAfter };
  }

  /** 代理只能操作直属玩家（parentId === 当前代理） */
  private async requireDirectPlayer(agentId: bigint, playerId: bigint) {
    const player = await this.prisma.user.findFirst({
      where: { id: playerId, userType: 'PLAYER', deletedAt: null },
      include: { auth: true, wallet: true, preferences: true },
    });
    if (!player) throw new NotFoundException('玩家不存在');
    if (player.parentId !== agentId) {
      throw new ForbiddenException('Can only manage direct players');
    }
    return player;
  }

  private async assertChildAgentWithinParent(
    parentAgentId: bigint,
    child: {
      creditLimit?: number | Decimal;
      cashbackRate?: number | Decimal;
      maxSingleDeposit?: number | Decimal | null;
      maxDailyDeposit?: number | Decimal | null;
    },
  ) {
    const parent = await this.prisma.agentProfile.findUnique({
      where: { userId: parentAgentId },
    });
    if (!parent) throw new BadRequestException('上级代理不存在');

    if (child.creditLimit !== undefined) {
      const limit = new Decimal(child.creditLimit);
      if (limit.lt(0)) throw new BadRequestException('授信额度不能为负');
      if (limit.gt(parent.creditLimit)) {
        throw new BadRequestException('下级代理授信不能超过上级授信额度');
      }
    }

    if (child.cashbackRate !== undefined) {
      const rate = new Decimal(child.cashbackRate);
      if (rate.lt(0)) throw new BadRequestException('回水比例不能为负');
      if (rate.gt(parent.cashbackRate)) {
        throw new BadRequestException('下级代理回水比例不能超过上级');
      }
    }

    if (child.maxSingleDeposit != null && parent.maxSingleDeposit != null) {
      if (new Decimal(child.maxSingleDeposit).gt(parent.maxSingleDeposit)) {
        throw new BadRequestException('下级代理单笔限额不能超过上级');
      }
    }
    if (child.maxSingleDeposit != null && new Decimal(child.maxSingleDeposit).lt(0)) {
      throw new BadRequestException('单笔限额不能为负');
    }

    if (child.maxDailyDeposit != null && parent.maxDailyDeposit != null) {
      if (new Decimal(child.maxDailyDeposit).gt(parent.maxDailyDeposit)) {
        throw new BadRequestException('下级代理日限额不能超过上级');
      }
    }
    if (child.maxDailyDeposit != null && new Decimal(child.maxDailyDeposit).lt(0)) {
      throw new BadRequestException('日限额不能为负');
    }
  }

  private resolveEffectiveDepositLimits(
    profile: {
      maxSingleDeposit: Decimal | null;
      maxDailyDeposit: Decimal | null;
    },
    parent?: { maxSingleDeposit: Decimal | null; maxDailyDeposit: Decimal | null } | null,
  ) {
    let maxSingleDeposit = profile.maxSingleDeposit;
    let maxDailyDeposit = profile.maxDailyDeposit;

    if (parent) {
      if (parent.maxSingleDeposit != null) {
        maxSingleDeposit =
          maxSingleDeposit != null
            ? Decimal.min(maxSingleDeposit, parent.maxSingleDeposit)
            : parent.maxSingleDeposit;
      }
      if (parent.maxDailyDeposit != null) {
        maxDailyDeposit =
          maxDailyDeposit != null
            ? Decimal.min(maxDailyDeposit, parent.maxDailyDeposit)
            : parent.maxDailyDeposit;
      }
    }

    return { maxSingleDeposit, maxDailyDeposit };
  }

  private normalizeOptionalLimit(value?: number | null) {
    if (value == null || value <= 0) return null;
    return new Decimal(value);
  }

  /** 玩家有所属代理时，上分金额不得超过该代理当前可用授信（会先重算 usedCredit） */
  async assertPlayerParentCreditForDeposit(playerId: bigint, amount: Decimal | number) {
    const user = await this.prisma.user.findFirst({
      where: { id: playerId, userType: 'PLAYER', deletedAt: null },
      select: { parentId: true },
    });
    if (!user?.parentId) return;

    await this.recalculateUsedCredit(user.parentId);
    const profile = await this.getProfile(user.parentId);
    const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
    const amt = new Decimal(amount);
    if (available.lt(amt)) {
      throw new BadRequestException('超过玩家上级代理可用授信，无法上分');
    }
  }

  /** 管理员给玩家上分：校验上级授信后入账，并刷新代理占用额度 */
  async adminDepositToPlayer(
    playerId: bigint,
    amount: number,
    operatorId: bigint,
    remark?: string,
    requestId?: string,
  ) {
    await this.assertPlayerParentCreditForDeposit(playerId, amount);
    const result = await this.wallet.deposit(
      playerId,
      amount,
      operatorId,
      remark,
      requestId,
    );
    const player = await this.prisma.user.findUnique({
      where: { id: playerId },
      select: { parentId: true },
    });
    if (player?.parentId) {
      await this.recalculateUsedCredit(player.parentId);
    }
    return result;
  }

  /** 上下分弹窗：玩家余额 + 授信代理可用额度/限额上下文 */
  async getPlayerTransferContext(
    playerId: bigint,
    options: { forAdmin?: boolean; actingAgentId?: bigint } = {},
  ) {
    const player = await this.prisma.user.findFirst({
      where: { id: playerId, userType: 'PLAYER', deletedAt: null },
      include: { wallet: true },
    });
    if (!player) throw new NotFoundException('玩家不存在');

    if (options.actingAgentId) {
      await this.requireDirectPlayer(options.actingAgentId, playerId);
    }

    const creditAgentId = options.forAdmin ? player.parentId : (options.actingAgentId ?? null);

    let credit: Record<string, unknown> | null = null;
    if (creditAgentId) {
      await this.recalculateUsedCredit(creditAgentId);
      const profile = await this.getProfile(creditAgentId);
      const parent = profile.parentAgentId
        ? await this.prisma.agentProfile.findUnique({ where: { userId: profile.parentAgentId } })
        : null;
      const { maxSingleDeposit, maxDailyDeposit } = this.resolveEffectiveDepositLimits(profile, parent);

      let dailyDepositUsed: string | null = null;
      if (!options.forAdmin) {
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        const dailyAgg = await this.prisma.walletTransaction.aggregate({
          where: {
            operatorId: creditAgentId,
            transactionType: 'MANUAL_DEPOSIT',
            createdAt: { gte: today },
          },
          _sum: { amount: true },
        });
        dailyDepositUsed = dec(dailyAgg._sum.amount);
      }

      const agentUser = await this.prisma.user.findUnique({
        where: { id: creditAgentId },
        select: { username: true },
      });

      credit = {
        agentId: creditAgentId.toString(),
        agentUsername: agentUser?.username ?? '',
        agentLevel: profile.level,
        creditLimit: dec(profile.creditLimit),
        usedCredit: dec(profile.usedCredit),
        availableCredit: dec(profile.availableCredit),
        maxSingleDeposit: maxSingleDeposit?.toString() ?? null,
        maxDailyDeposit: maxDailyDeposit?.toString() ?? null,
        dailyDepositUsed,
        appliesDepositLimits: !options.forAdmin,
      };
    }

    return {
      player: {
        id: player.id.toString(),
        username: player.username,
        availableBalance: dec(player.wallet?.availableBalance),
        frozenBalance: dec(player.wallet?.frozenBalance),
      },
      credit,
    };
  }

  private async assertAgentDepositLimits(creditAgentId: bigint, amount: Decimal) {
    const profile = await this.prisma.agentProfile.findUnique({
      where: { userId: creditAgentId },
    });
    if (!profile) return;

    const parent = profile.parentAgentId
      ? await this.prisma.agentProfile.findUnique({
          where: { userId: profile.parentAgentId },
        })
      : null;
    const { maxSingleDeposit, maxDailyDeposit } = this.resolveEffectiveDepositLimits(profile, parent);

    if (maxSingleDeposit && amount.gt(maxSingleDeposit)) {
      throw new BadRequestException('超过代理单笔上分限额');
    }

    if (maxDailyDeposit) {
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      const dailyAgg = await this.prisma.walletTransaction.aggregate({
        where: {
          operatorId: creditAgentId,
          transactionType: 'MANUAL_DEPOSIT',
          createdAt: { gte: today },
        },
        _sum: { amount: true },
      });
      const dailyTotal = new Decimal(dailyAgg._sum.amount ?? 0).add(amount);
      if (dailyTotal.gt(maxDailyDeposit)) {
        throw new BadRequestException('超过代理日上分限额');
      }
    }
  }

  private async assertChildCreditWithinParent(
    parentAgentId: bigint,
    childProfile: { userId: bigint; creditLimit: Decimal; usedCredit: Decimal },
    creditAfter: Decimal,
  ) {
    await this.assertChildAgentWithinParent(parentAgentId, { creditLimit: creditAfter });

    const parent = await this.getProfile(parentAgentId);
    const parentAvailable = new Decimal(parent.creditLimit).sub(parent.usedCredit);
    const oldExposure = Decimal.max(childProfile.creditLimit, childProfile.usedCredit);
    const newExposure = Decimal.max(creditAfter, childProfile.usedCredit);
    const exposureDelta = newExposure.sub(oldExposure);
    if (exposureDelta.gt(0) && exposureDelta.gt(parentAvailable)) {
      throw new BadRequestException('上级可用授信不足');
    }
  }

  async depositToPlayer(
    agentId: bigint,
    playerId: bigint,
    amount: number,
    requestId: string,
    remark?: string,
  ) {
    await this.requireDirectPlayer(agentId, playerId);

    const profile = await this.getProfile(agentId);
    const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
    const amt = new Decimal(amount);

    if (available.lt(amt)) {
      throw new BadRequestException('Insufficient agent credit');
    }

    await this.assertAgentDepositLimits(agentId, amt);

    await this.wallet.deposit(playerId, amt, agentId, remark ?? 'Agent deposit', requestId);
    await this.recalculateUsedCredit(agentId);

    return { success: true };
  }

  async withdrawFromPlayer(
    agentId: bigint,
    playerId: bigint,
    amount: number,
    requestId: string,
  ) {
    await this.requireDirectPlayer(agentId, playerId);

    await this.wallet.withdraw(playerId, amount, agentId, 'Agent withdraw', requestId);
    await this.recalculateUsedCredit(agentId);

    return { success: true };
  }

  async getDirectPlayerDetail(agentId: bigint, playerId: bigint) {
    const user = await this.requireDirectPlayer(agentId, playerId);

    const [betCount, betStake] = await Promise.all([
      this.prisma.bet.count({ where: { userId: playerId } }),
      this.prisma.bet.aggregate({
        where: { userId: playerId },
        _sum: { stake: true, actualReturn: true },
      }),
    ]);

    return {
      id: user.id.toString(),
      username: user.username,
      status: user.status,
      phone: user.preferences?.phone ?? null,
      email: user.preferences?.email ?? null,
      managedPassword: user.preferences?.managedPassword ?? null,
      availableBalance: user.wallet?.availableBalance?.toString() ?? '0',
      frozenBalance: user.wallet?.frozenBalance?.toString() ?? '0',
      lastLoginAt: user.auth?.lastLoginAt ?? null,
      loginFailCount: user.auth?.loginFailCount ?? 0,
      betCount,
      totalStake: betStake._sum.stake?.toString() ?? '0',
      totalReturn: betStake._sum.actualReturn?.toString() ?? '0',
      createdAt: user.createdAt,
    };
  }

  async updateDirectPlayer(
    agentId: bigint,
    playerId: bigint,
    data: {
      username?: string;
      password?: string;
      phone?: string;
      email?: string;
      status?: string;
    },
  ) {
    const user = await this.requireDirectPlayer(agentId, playerId);

    if (data.status && !['ACTIVE', 'SUSPENDED'].includes(data.status)) {
      throw new BadRequestException('无效状态');
    }

    if (data.username !== undefined) {
      const nextUsername = data.username.trim();
      if (!nextUsername) throw new BadRequestException('账号名称不能为空');
      try {
        assertPlayerUsername(nextUsername);
      } catch (e) {
        throw new BadRequestException(e instanceof Error ? e.message : '玩家用户名格式无效');
      }
      if (nextUsername !== user.username) {
        const taken = await this.prisma.user.findUnique({ where: { username: nextUsername } });
        if (taken) throw new BadRequestException('账号名称已被占用');
        await this.prisma.user.update({
          where: { id: playerId },
          data: { username: nextUsername },
        });
      }
    }

    if (data.password !== undefined) {
      const nextPassword = data.password;
      if (nextPassword.length < 8) throw new BadRequestException('密码至少 8 位');
      if (!user.auth) throw new BadRequestException('账号认证信息缺失');
      const hash = await bcrypt.hash(nextPassword, 10);
      await this.prisma.userAuth.update({
        where: { userId: playerId },
        data: { passwordHash: hash, loginFailCount: 0, lockedUntil: null },
      });
      await this.prisma.userPreference.upsert({
        where: { userId: playerId },
        create: { userId: playerId, managedPassword: nextPassword },
        update: { managedPassword: nextPassword },
      });
    }

    if (data.status) {
      await this.prisma.user.update({
        where: { id: playerId },
        data: { status: data.status },
      });
    }

    const prefPatch: { phone?: string | null; email?: string | null } = {};
    if (data.phone !== undefined) prefPatch.phone = data.phone.trim() || null;
    if (data.email !== undefined) prefPatch.email = data.email.trim() || null;

    if (Object.keys(prefPatch).length > 0) {
      await this.prisma.userPreference.upsert({
        where: { userId: playerId },
        create: {
          userId: playerId,
          phone: prefPatch.phone ?? null,
          email: prefPatch.email ?? null,
        },
        update: prefPatch,
      });
    }

    return this.getDirectPlayerDetail(agentId, playerId);
  }

  async listAgentsAdmin(params?: {
    page?: number;
    pageSize?: number;
    keyword?: string;
    status?: string;
    level?: 1 | 2;
    parentAgentId?: bigint;
  }) {
    const page = Math.max(1, params?.page ?? 1);
    const pageSize = Math.min(Math.max(1, params?.pageSize ?? 10), 100);
    const skip = (page - 1) * pageSize;

    const where: Prisma.AgentProfileWhereInput = {};
    if (params?.level === 2) {
      where.level = 2;
    } else if (params?.level === 1) {
      where.level = 1;
    } else if (params?.parentAgentId !== undefined) {
      where.parentAgentId = params.parentAgentId;
    } else {
      where.parentAgentId = null;
    }
    const kw = params?.keyword?.trim();
    const status = params?.status?.trim();
    const userWhere: Prisma.UserWhereInput = {};
    if (status && ['ACTIVE', 'SUSPENDED'].includes(status)) {
      userWhere.status = status;
    }
    if (kw) {
      userWhere.username = { contains: kw, mode: 'insensitive' };
    }
    if (Object.keys(userWhere).length > 0) {
      where.user = userWhere;
    }

    const [profiles, total] = await Promise.all([
      this.prisma.agentProfile.findMany({
        where,
        include: {
          user: { include: { preferences: true } },
        },
        orderBy: { createdAt: 'desc' },
        skip,
        take: pageSize,
      }),
      this.prisma.agentProfile.count({ where }),
    ]);

    const agentIds = profiles.map((p) => p.userId);
    const playerCounts =
      agentIds.length > 0
        ? await this.prisma.user.groupBy({
            by: ['parentId'],
            where: {
              userType: 'PLAYER',
              parentId: { in: agentIds },
              deletedAt: null,
            },
            _count: { _all: true },
          })
        : [];

    const countMap = new Map(
      playerCounts.map((g) => [g.parentId?.toString(), g._count._all]),
    );

    const childAgentCounts =
      agentIds.length > 0
        ? await this.prisma.agentProfile.groupBy({
            by: ['parentAgentId'],
            where: { parentAgentId: { in: agentIds } },
            _count: { _all: true },
          })
        : [];
    const childAgentCountMap = new Map(
      childAgentCounts.map((g) => [g.parentAgentId?.toString(), g._count._all]),
    );

    const parentAgentIds = [
      ...new Set(profiles.map((p) => p.parentAgentId).filter((id): id is bigint => id != null)),
    ];
    const parentUsers =
      parentAgentIds.length > 0
        ? await this.prisma.user.findMany({
            where: { id: { in: parentAgentIds } },
            select: { id: true, username: true },
          })
        : [];
    const parentUsernameMap = new Map(parentUsers.map((u) => [u.id.toString(), u.username]));

    const items = profiles.map((p) => {
      const available = new Decimal(p.creditLimit).sub(p.usedCredit);
      return {
        id: p.id.toString(),
        userId: p.userId.toString(),
        username: p.user.username,
        userStatus: p.user.status,
        level: p.level,
        status: p.status,
        parentAgentId: p.parentAgentId?.toString() ?? null,
        parentUsername: p.parentAgentId
          ? parentUsernameMap.get(p.parentAgentId.toString()) ?? null
          : null,
        creditLimit: p.creditLimit.toString(),
        usedCredit: p.usedCredit.toString(),
        availableCredit: available.toString(),
        directPlayerLiability: p.directPlayerLiability.toString(),
        childAgentExposure: p.childAgentExposure.toString(),
        cashbackRate: p.cashbackRate.toString(),
        maxSingleDeposit: p.maxSingleDeposit?.toString() ?? null,
        maxDailyDeposit: p.maxDailyDeposit?.toString() ?? null,
        directPlayerCount: countMap.get(p.userId.toString()) ?? 0,
        childAgentCount: childAgentCountMap.get(p.userId.toString()) ?? 0,
        phone: p.user.preferences?.phone ?? null,
        email: p.user.preferences?.email ?? null,
        locale: p.user.locale,
        createdAt: p.createdAt,
        updatedAt: p.updatedAt,
      };
    });

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

  async getAgentAdminDetail(agentId: bigint) {
    const profile = await this.prisma.agentProfile.findUnique({
      where: { userId: agentId },
      include: { user: { include: { preferences: true, auth: true } } },
    });
    if (!profile) throw new NotFoundException('代理不存在');

    const [directPlayerCount, childAgentCount, recentCredits] = await Promise.all([
      this.prisma.user.count({
        where: { parentId: agentId, userType: 'PLAYER', deletedAt: null },
      }),
      this.prisma.agentProfile.count({
        where: { parentAgentId: agentId },
      }),
      this.prisma.agentCreditTransaction.findMany({
        where: { agentId },
        orderBy: { createdAt: 'desc' },
        take: 10,
      }),
    ]);

    const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
    let parentUsername: string | null = null;
    if (profile.parentAgentId) {
      const parent = await this.prisma.user.findUnique({
        where: { id: profile.parentAgentId },
        select: { username: true },
      });
      parentUsername = parent?.username ?? null;
    }

    return {
      id: profile.id.toString(),
      userId: profile.userId.toString(),
      username: profile.user.username,
      userStatus: profile.user.status,
      level: profile.level,
      status: profile.status,
      parentAgentId: profile.parentAgentId?.toString() ?? null,
      parentUsername,
      creditLimit: profile.creditLimit.toString(),
      usedCredit: profile.usedCredit.toString(),
      availableCredit: available.toString(),
      directPlayerLiability: profile.directPlayerLiability.toString(),
      childAgentExposure: profile.childAgentExposure.toString(),
      cashbackRate: profile.cashbackRate.toString(),
      maxSingleDeposit: profile.maxSingleDeposit?.toString() ?? null,
      maxDailyDeposit: profile.maxDailyDeposit?.toString() ?? null,
      directPlayerCount,
      childAgentCount,
      phone: profile.user.preferences?.phone ?? null,
      email: profile.user.preferences?.email ?? null,
      managedPassword: profile.user.preferences?.managedPassword ?? null,
      locale: profile.user.locale,
      lastLoginAt: profile.user.auth?.lastLoginAt ?? null,
      loginFailCount: profile.user.auth?.loginFailCount ?? 0,
      createdAt: profile.createdAt,
      updatedAt: profile.updatedAt,
      recentCreditTransactions: recentCredits.map((t) => ({
        id: t.id.toString(),
        transactionType: t.transactionType,
        amount: t.amount.toString(),
        creditBefore: t.creditBefore.toString(),
        creditAfter: t.creditAfter.toString(),
        remark: t.remark,
        createdAt: t.createdAt,
      })),
    };
  }

  async listCreditTransactions(params: {
    page?: number;
    pageSize?: number;
    agentId?: bigint;
    keyword?: string;
    transactionType?: string;
  }) {
    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.AgentCreditTransactionWhereInput = {};

    if (params.transactionType?.trim()) {
      where.transactionType = params.transactionType.trim();
    }

    const keyword = params.keyword?.trim();
    if (keyword) {
      const matched = await this.prisma.user.findMany({
        where: {
          userType: 'AGENT',
          deletedAt: null,
          username: { contains: keyword, mode: 'insensitive' },
        },
        select: { id: true },
        take: 50,
      });
      const agentUserIds = matched.map((u) => u.id);
      if (!agentUserIds.length) {
        return { items: [], total: 0, page, pageSize };
      }
      if (params.agentId) {
        if (!agentUserIds.some((id) => id === params.agentId)) {
          return { items: [], total: 0, page, pageSize };
        }
        where.agentId = params.agentId;
      } else {
        where.agentId = { in: agentUserIds };
      }
    } else if (params.agentId) {
      where.agentId = params.agentId;
    }

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

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

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

    const agentNameById = new Map(agentUsers.map((u) => [u.id.toString(), u.username]));
    const operatorNameById = new Map(operators.map((u) => [u.id.toString(), u.username]));

    return {
      items: rows.map((row) => ({
        id: row.id.toString(),
        agentId: row.agentId.toString(),
        agentUsername: agentNameById.get(row.agentId.toString()) ?? null,
        transactionType: row.transactionType,
        amount: row.amount.toString(),
        creditBefore: row.creditBefore.toString(),
        creditAfter: row.creditAfter.toString(),
        referenceType: row.referenceType,
        referenceId: row.referenceId,
        operatorId: row.operatorId?.toString() ?? null,
        operatorUsername: row.operatorId
          ? (operatorNameById.get(row.operatorId.toString()) ?? null)
          : null,
        requestId: row.requestId,
        remark: row.remark,
        createdAt: row.createdAt,
      })),
      total,
      page,
      pageSize,
    };
  }

  async updateAgentAdmin(
    agentId: bigint,
    data: {
      status?: string;
      locale?: string;
      phone?: string;
      email?: string;
      cashbackRate?: number;
      maxSingleDeposit?: number | null;
      maxDailyDeposit?: number | null;
      username?: string;
      password?: string;
      freezeDirectPlayers?: boolean;
    },
  ) {
    const profile = await this.prisma.agentProfile.findUnique({
      where: { userId: agentId },
      include: { user: true },
    });
    if (!profile) throw new NotFoundException('代理不存在');

    if (data.status && !['ACTIVE', 'SUSPENDED'].includes(data.status)) {
      throw new BadRequestException('无效状态');
    }

    // Handle username change
    if (data.username !== undefined) {
      const nextUsername = data.username.trim();
      if (!nextUsername) throw new BadRequestException('账号名称不能为空');
      if (nextUsername !== profile.user.username) {
        const taken = await this.prisma.user.findUnique({ where: { username: nextUsername } });
        if (taken) throw new BadRequestException('账号名称已被占用');
        await this.prisma.user.update({
          where: { id: agentId },
          data: { username: nextUsername },
        });
      }
    }

    // Handle password change
    if (data.password !== undefined) {
      const nextPassword = data.password;
      if (nextPassword.length < 8) throw new BadRequestException('密码至少 8 位');
      const hash = await bcrypt.hash(nextPassword, 10);
      await this.prisma.userAuth.upsert({
        where: { userId: agentId },
        create: { userId: agentId, passwordHash: hash, loginFailCount: 0, lockedUntil: null },
        update: { passwordHash: hash, loginFailCount: 0, lockedUntil: null },
      });
      await this.prisma.userPreference.upsert({
        where: { userId: agentId },
        create: { userId: agentId, managedPassword: nextPassword },
        update: { managedPassword: nextPassword },
      });
    }

    // Handle status change (with optional cascade freeze)
    if (data.status) {
      await this.prisma.$transaction([
        this.prisma.user.update({
          where: { id: agentId },
          data: { status: data.status },
        }),
        this.prisma.agentProfile.update({
          where: { userId: agentId },
          data: { status: data.status },
        }),
      ]);

      // 级联冻结：需后台开启且管理员/操作方显式勾选（MVP 默认不冻结玩家）
      const suspendSettings = await this.systemConfig.getAgentSuspendSettings();
      if (
        data.status === 'SUSPENDED' &&
        data.freezeDirectPlayers &&
        suspendSettings.suspendFreezeDirectPlayers
      ) {
        await this.prisma.user.updateMany({
          where: { parentId: agentId, userType: 'PLAYER', deletedAt: null },
          data: { status: 'SUSPENDED' },
        });
      }
    }

    if (data.locale) {
      await this.prisma.user.update({
        where: { id: agentId },
        data: { locale: data.locale },
      });
    }

    if (data.cashbackRate !== undefined) {
      if (profile.parentAgentId) {
        await this.assertChildAgentWithinParent(profile.parentAgentId, {
          cashbackRate: data.cashbackRate,
        });
      }
      await this.prisma.agentProfile.update({
        where: { userId: agentId },
        data: { cashbackRate: data.cashbackRate },
      });
    }

    const limitPatch: {
      maxSingleDeposit?: Decimal | null;
      maxDailyDeposit?: Decimal | null;
    } = {};
    if (data.maxSingleDeposit !== undefined) {
      limitPatch.maxSingleDeposit = this.normalizeOptionalLimit(data.maxSingleDeposit);
    }
    if (data.maxDailyDeposit !== undefined) {
      limitPatch.maxDailyDeposit = this.normalizeOptionalLimit(data.maxDailyDeposit);
    }
    if (Object.keys(limitPatch).length > 0) {
      if (profile.parentAgentId) {
        await this.assertChildAgentWithinParent(profile.parentAgentId, {
          maxSingleDeposit: limitPatch.maxSingleDeposit ?? undefined,
          maxDailyDeposit: limitPatch.maxDailyDeposit ?? undefined,
        });
      }
      await this.prisma.agentProfile.update({
        where: { userId: agentId },
        data: limitPatch,
      });
    }

    if (data.phone !== undefined || data.email !== undefined || data.locale) {
      const phone = data.phone !== undefined ? data.phone?.trim() || null : undefined;
      const email = data.email !== undefined ? data.email?.trim() || null : undefined;
      await this.prisma.userPreference.upsert({
        where: { userId: agentId },
        create: {
          userId: agentId,
          locale: data.locale ?? profile.user.locale,
          phone: phone ?? null,
          email: email ?? null,
        },
        update: {
          ...(data.locale ? { locale: data.locale } : {}),
          ...(phone !== undefined ? { phone } : {}),
          ...(email !== undefined ? { email } : {}),
        },
      });
    }

    return this.getAgentAdminDetail(agentId);
  }

  /** 可升级为一级代理的玩家（尚无代理档案） */
  async listPromotablePlayers(keyword?: string) {
    const q = keyword?.trim();
    return this.prisma.user.findMany({
      where: {
        userType: 'PLAYER',
        deletedAt: null,
        agentProfile: null,
        ...(q
          ? { username: { contains: q, mode: 'insensitive' } }
          : {}),
      },
      select: {
        id: true,
        username: true,
        status: true,
        parentId: true,
        preferences: { select: { phone: true, email: true } },
        parent: { select: { username: true } },
      },
      orderBy: { id: 'desc' },
      take: 50,
    });
  }

  /** 将已有玩家账号升级为一级代理（不新建用户） */
  async promotePlayerToTier1Agent(
    userId: bigint,
    data: {
      creditLimit: number;
      cashbackRate?: number;
      maxSingleDeposit?: number | null;
      maxDailyDeposit?: number | null;
      phone?: string;
      email?: string;
    },
  ) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      include: { agentProfile: true, preferences: true },
    });
    if (!user || user.deletedAt) {
      throw new NotFoundException('用户不存在');
    }
    if (user.userType !== 'PLAYER') {
      throw new BadRequestException('仅玩家账号可设为代理');
    }
    if (user.agentProfile) {
      throw new BadRequestException('该用户已是代理');
    }

    const oldParentId = user.parentId;
    const phone =
      data.phone !== undefined
        ? data.phone.trim() || null
        : user.preferences?.phone ?? null;
    const email =
      data.email !== undefined
        ? data.email.trim() || null
        : user.preferences?.email ?? null;

    await this.prisma.$transaction(async (tx) => {
      await tx.user.update({
        where: { id: userId },
        data: {
          userType: 'AGENT',
          agentLevel: 1,
          parentId: null,
        },
      });

      if (user.preferences) {
        await tx.userPreference.update({
          where: { userId },
          data: { phone, email },
        });
      } else {
        await tx.userPreference.create({
          data: {
            userId,
            locale: user.locale,
            phone,
            email,
          },
        });
      }

      await tx.agentProfile.create({
        data: {
          userId,
          level: 1,
          parentAgentId: null,
          creditLimit: data.creditLimit,
          cashbackRate: data.cashbackRate ?? 0,
          maxSingleDeposit: this.normalizeOptionalLimit(data.maxSingleDeposit),
          maxDailyDeposit: this.normalizeOptionalLimit(data.maxDailyDeposit),
        },
      });

      await tx.agentClosure.create({
        data: { ancestorId: userId, descendantId: userId, depth: 0 },
      });
    });

    if (oldParentId) {
      await this.recalculateUsedCredit(oldParentId);
    }

    const updated = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!updated) {
      throw new NotFoundException('用户不存在');
    }
    return updated;
  }

  async createAgent(
    operatorId: bigint,
    data: {
      username: string;
      password: string;
      level: number;
      parentAgentId?: bigint;
      creditLimit?: number;
      locale?: string;
      phone?: string;
      email?: string;
      cashbackRate?: number;
      maxSingleDeposit?: number | null;
      maxDailyDeposit?: number | null;
    },
  ) {
    if (data.level !== 1 && data.level !== 2) {
      throw new BadRequestException('Agent level must be 1 or 2');
    }
    if (data.level === 2 && !data.parentAgentId) {
      throw new BadRequestException('Level 2 agent requires parent');
    }

    if (data.parentAgentId) {
      await this.assertChildAgentWithinParent(data.parentAgentId, {
        creditLimit: data.creditLimit ?? 0,
        cashbackRate: data.cashbackRate ?? 0,
        maxSingleDeposit: data.maxSingleDeposit,
        maxDailyDeposit: data.maxDailyDeposit,
      });
    }

    const maxSingleDeposit = this.normalizeOptionalLimit(data.maxSingleDeposit);
    const maxDailyDeposit = this.normalizeOptionalLimit(data.maxDailyDeposit);

    const hash = await this.auth.hashPassword(data.password);

    return this.prisma.$transaction(async (tx) => {
      const locale = data.locale ?? 'zh-CN';
      const user = await tx.user.create({
        data: {
          username: data.username,
          userType: 'AGENT',
          parentId: data.parentAgentId,
          agentLevel: data.level,
          locale,
        },
      });

      await tx.userAuth.create({
        data: { userId: user.id, passwordHash: hash },
      });

      await tx.userPreference.create({
        data: {
          userId: user.id,
          locale,
          phone: data.phone?.trim() || null,
          email: data.email?.trim() || null,
        },
      });

      await tx.agentProfile.create({
        data: {
          userId: user.id,
          level: data.level,
          parentAgentId: data.parentAgentId,
          creditLimit: data.creditLimit ?? 0,
          cashbackRate: data.cashbackRate ?? 0,
          maxSingleDeposit,
          maxDailyDeposit,
        },
      });

      // Build closure table
      await tx.agentClosure.create({
        data: { ancestorId: user.id, descendantId: user.id, depth: 0 },
      });

      if (data.parentAgentId) {
        const ancestors = await tx.agentClosure.findMany({
          where: { descendantId: data.parentAgentId },
        });
        for (const a of ancestors) {
          await tx.agentClosure.create({
            data: {
              ancestorId: a.ancestorId,
              descendantId: user.id,
              depth: a.depth + 1,
            },
          });
        }
        if (data.parentAgentId) {
          await this.recalculateUsedCredit(data.parentAgentId);
        }
      }

      return user;
    });
  }

  async createPlayer(
    operatorId: bigint,
    data: {
      username: string;
      password: string;
      parentId?: bigint;
      locale?: string;
      phone?: string;
      email?: string;
      initialDeposit?: number;
      depositRemark?: string;
      depositRequestId?: string;
      asTier1Agent?: boolean;
      asSubAgent?: boolean;
      parentAgentId?: bigint;
      creditLimit?: number;
      cashbackRate?: number;
      maxSingleDeposit?: number | null;
      maxDailyDeposit?: number | null;
    },
  ) {
    if (data.asTier1Agent) {
      if (data.parentId != null) {
        throw new BadRequestException('一级代理不可设置上级玩家');
      }
      if (data.initialDeposit && data.initialDeposit > 0) {
        throw new BadRequestException('设为代理时请使用授信额度，勿填玩家初始余额');
      }
      return this.createAgent(operatorId, {
        username: data.username,
        password: data.password,
        level: 1,
        creditLimit: data.creditLimit ?? 0,
        cashbackRate: data.cashbackRate ?? 0,
        locale: data.locale,
        phone: data.phone,
        email: data.email,
      });
    }

    if (data.asSubAgent) {
      if (data.parentAgentId == null && data.parentId == null) {
        throw new BadRequestException('二级代理必须指定上级代理');
      }
      if (data.initialDeposit && data.initialDeposit > 0) {
        throw new BadRequestException('设为代理时请使用授信额度，勿填玩家初始余额');
      }
      const parentAgentId = data.parentAgentId ?? data.parentId;
      return this.createAgent(operatorId, {
        username: data.username,
        password: data.password,
        level: 2,
        parentAgentId,
        creditLimit: data.creditLimit ?? 0,
        cashbackRate: data.cashbackRate ?? 0,
        maxSingleDeposit: data.maxSingleDeposit,
        maxDailyDeposit: data.maxDailyDeposit,
        locale: data.locale,
        phone: data.phone,
        email: data.email,
      });
    }

    let parentId: bigint | null = null;
    if (data.parentId != null) {
      const parent = await this.prisma.user.findUnique({ where: { id: data.parentId } });
      if (!parent || parent.userType !== 'AGENT') {
        throw new BadRequestException('上级必须为代理账号');
      }
      parentId = data.parentId;

      const operator = await this.prisma.user.findUnique({ where: { id: operatorId } });
      if (operator?.userType === 'AGENT' && parentId !== operatorId) {
        throw new ForbiddenException('Can only create direct players');
      }
    }

    try {
      assertPlayerUsername(data.username);
    } catch (e) {
      throw new BadRequestException(e instanceof Error ? e.message : '玩家用户名格式无效');
    }

    const hash = await this.auth.hashPassword(data.password);
    const locale = data.locale ?? 'zh-CN';

    const user = await this.prisma.$transaction(async (tx) => {
      const created = await tx.user.create({
        data: {
          username: data.username,
          userType: 'PLAYER',
          parentId,
          locale,
        },
      });

      await tx.userAuth.create({
        data: { userId: created.id, passwordHash: hash },
      });

      await tx.wallet.create({
        data: { userId: created.id },
      });

      await tx.userPreference.create({
        data: {
          userId: created.id,
          locale,
          phone: data.phone?.trim() || null,
          email: data.email?.trim() || null,
          managedPassword: data.password,
        },
      });

      return created;
    });

    if (parentId) {
      await this.recalculateUsedCredit(parentId);
    }

    const initial = data.initialDeposit ?? 0;
    if (initial > 0) {
      const requestId =
        data.depositRequestId ?? `admin-create-${user.id}-${Date.now()}`;
      await this.assertPlayerParentCreditForDeposit(user.id, initial);
      await this.wallet.deposit(
        user.id,
        initial,
        operatorId,
        data.depositRemark ?? '开户初始余额',
        requestId,
      );
      if (parentId) {
        await this.recalculateUsedCredit(parentId);
      }
    }

    return user;
  }

  async getDirectPlayers(agentId: bigint) {
    return this.prisma.user.findMany({
      where: { parentId: agentId, userType: 'PLAYER' },
      include: { wallet: true },
      orderBy: { createdAt: 'desc' },
    });
  }

  async getChildAgents(agentId: bigint) {
    return this.prisma.agentProfile.findMany({
      where: { parentAgentId: agentId },
      include: { user: true },
    });
  }

  async listChildAgentsSummary(parentAgentId: bigint) {
    const profiles = await this.getChildAgents(parentAgentId);
    const agentIds = profiles.map((p) => p.userId);
    const playerCounts =
      agentIds.length > 0
        ? await this.prisma.user.groupBy({
            by: ['parentId'],
            where: {
              userType: 'PLAYER',
              parentId: { in: agentIds },
              deletedAt: null,
            },
            _count: { _all: true },
          })
        : [];

    const countMap = new Map(
      playerCounts.map((g) => [g.parentId?.toString(), g._count._all]),
    );

    return profiles.map((p) => {
      const available = new Decimal(p.creditLimit).sub(p.usedCredit);
      return {
        userId: p.userId.toString(),
        username: p.user.username,
        userStatus: p.user.status,
        status: p.status,
        level: p.level,
        creditLimit: dec(p.creditLimit),
        usedCredit: dec(p.usedCredit),
        availableCredit: available.toString(),
        directPlayerCount: countMap.get(p.userId.toString()) ?? 0,
        createdAt: p.createdAt,
      };
    });
  }

  async assertDirectChildAgent(parentAgentId: bigint, subAgentId: bigint) {
    const profile = await this.prisma.agentProfile.findUnique({
      where: { userId: subAgentId },
    });
    if (!profile || profile.parentAgentId !== parentAgentId) {
      throw new ForbiddenException('Not your sub-agent');
    }
    return profile;
  }

  async getSubAgentForParent(parentAgentId: bigint, subAgentId: bigint) {
    await this.assertDirectChildAgent(parentAgentId, subAgentId);
    return this.getAgentAdminDetail(subAgentId);
  }

  async updateSubAgentForParent(
    parentAgentId: bigint,
    subAgentId: bigint,
    data: {
      username?: string;
      password?: string;
      phone?: string;
      email?: string;
      status?: string;
      freezeDirectPlayers?: boolean;
    },
  ) {
    await this.assertDirectChildAgent(parentAgentId, subAgentId);
    const { freezeDirectPlayers: _ignored, ...safeData } = data;
    return this.updateAgentAdmin(subAgentId, safeData);
  }

  async getSubtreeAgentIds(agentId: bigint) {
    const descendants = await this.prisma.agentClosure.findMany({
      where: { ancestorId: agentId },
      select: { descendantId: true },
    });
    return descendants.map((d) => d.descendantId);
  }

  async getReportSummary(agentId: bigint) {
    const profile = await this.getProfile(agentId);
    const agentIds = await this.getSubtreeAgentIds(agentId);
    const betScope = { agentId: { in: agentIds } };
    const playerWhere = {
      parentId: agentId,
      userType: 'PLAYER' as const,
      deletedAt: null,
    };

    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const yesterday = new Date(today.getTime() - 86400000);

    const trend7d = await Promise.all(
      Array.from({ length: 7 }, (_, i) => {
        const dayStart = new Date(today);
        dayStart.setDate(dayStart.getDate() - (6 - i));
        const dayEnd = new Date(dayStart);
        dayEnd.setDate(dayEnd.getDate() + 1);
        return this.prisma.bet
          .aggregate({
            where: { ...betScope, placedAt: { gte: dayStart, lt: dayEnd } },
            _sum: { stake: true, actualReturn: true },
            _count: true,
          })
          .then((agg) => ({
            date: dayStart.toISOString().slice(0, 10),
            label: `${dayStart.getMonth() + 1}/${dayStart.getDate()}`,
            betCount: agg._count,
            stake: dec(agg._sum.stake),
            payout: dec(agg._sum.actualReturn),
            ggr: sub(agg._sum.stake, agg._sum.actualReturn),
          }));
      }),
    );

    const [
      todayBets,
      yesterdayBets,
      pendingBets,
      betStatusToday,
      playerTotal,
      playerActive,
      playerSuspended,
      newPlayersToday,
      subAgentTotal,
      subAgentsActive,
      walletAgg,
      recentBets,
      recentPlayers,
    ] = await Promise.all([
      this.prisma.bet.aggregate({
        where: { ...betScope, placedAt: { gte: today } },
        _sum: { stake: true, actualReturn: true },
        _count: true,
      }),
      this.prisma.bet.aggregate({
        where: { ...betScope, placedAt: { gte: yesterday, lt: today } },
        _sum: { stake: true, actualReturn: true },
        _count: true,
      }),
      this.prisma.bet.count({ where: { ...betScope, status: 'PENDING' } }),
      this.prisma.bet.groupBy({
        by: ['status'],
        where: { ...betScope, placedAt: { gte: today } },
        _count: { _all: true },
        _sum: { stake: true },
      }),
      this.prisma.user.count({ where: playerWhere }),
      this.prisma.user.count({ where: { ...playerWhere, status: 'ACTIVE' } }),
      this.prisma.user.count({ where: { ...playerWhere, status: 'SUSPENDED' } }),
      this.prisma.user.count({
        where: { ...playerWhere, createdAt: { gte: today } },
      }),
      this.prisma.agentProfile.count({ where: { parentAgentId: agentId } }),
      this.prisma.agentProfile.count({
        where: { parentAgentId: agentId, status: 'ACTIVE' },
      }),
      this.prisma.wallet.aggregate({
        where: { user: playerWhere },
        _sum: { availableBalance: true, frozenBalance: true },
        _count: { _all: true },
      }),
      this.prisma.bet.findMany({
        where: betScope,
        take: 8,
        orderBy: { placedAt: 'desc' },
        include: { user: { select: { username: true } } },
      }),
      this.prisma.user.findMany({
        where: playerWhere,
        take: 6,
        orderBy: { createdAt: 'desc' },
        select: {
          id: true,
          username: true,
          status: true,
          createdAt: true,
        },
      }),
    ]);

    const todayBetByStatus: Record<string, { count: number; stake: string }> = {};
    for (const g of betStatusToday) {
      todayBetByStatus[g.status] = {
        count: g._count._all,
        stake: dec(g._sum.stake),
      };
    }

    const creditLimit = profile.creditLimit ?? new Decimal(0);
    const usedCredit = profile.usedCredit ?? new Decimal(0);
    const availableCredit = new Decimal(creditLimit).sub(usedCredit);

    return {
      generatedAt: new Date().toISOString(),
      trend7d,
      today: {
        betCount: todayBets._count,
        stake: dec(todayBets._sum.stake),
        payout: dec(todayBets._sum.actualReturn),
        ggr: sub(todayBets._sum.stake, todayBets._sum.actualReturn),
        newPlayers: newPlayersToday,
      },
      yesterday: {
        betCount: yesterdayBets._count,
        stake: dec(yesterdayBets._sum.stake),
        payout: dec(yesterdayBets._sum.actualReturn),
        ggr: sub(yesterdayBets._sum.stake, yesterdayBets._sum.actualReturn),
      },
      players: {
        directTotal: playerTotal,
        active: playerActive,
        suspended: playerSuspended,
        newToday: newPlayersToday,
      },
      subAgents: {
        total: subAgentTotal,
        active: subAgentsActive,
      },
      wallets: {
        totalAvailable: dec(walletAgg._sum.availableBalance),
        totalFrozen: dec(walletAgg._sum.frozenBalance),
        playerWalletCount: walletAgg._count._all,
      },
      credit: {
        creditLimit: dec(creditLimit),
        usedCredit: dec(usedCredit),
        availableCredit: availableCredit.toString(),
        directPlayerLiability: dec(profile.directPlayerLiability),
        childAgentExposure: dec(profile.childAgentExposure),
      },
      bets: {
        pendingTotal: pendingBets,
        todayByStatus: todayBetByStatus,
      },
      recentBets: recentBets.map((b) => ({
        betNo: b.betNo,
        username: b.user.username,
        stake: dec(b.stake),
        status: b.status,
        placedAt: b.placedAt,
      })),
      recentPlayers: recentPlayers.map((p) => ({
        id: p.id.toString(),
        username: p.username,
        status: p.status,
        createdAt: p.createdAt,
      })),
    };
  }
}
