import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Decimal } from '@prisma/client/runtime/library';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { FundsPostingService } from '../ledger/funds-posting.service';
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';

type TxClient = Prisma.TransactionClient;
type PrismaClientLike = PrismaService | TxClient;

const AGENT_DAILY_DEPOSIT_TX_TYPES = ['AGENT_DEPOSIT', 'MANUAL_DEPOSIT'] as const;

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

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

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

  async recalculateUsedCredit(agentId: bigint, tx?: TxClient) {
    const client = tx ?? this.prisma;
    const cascadeIds = await this.getCreditCascadeIds(agentId, client);
    let requestedUsedCredit = new Decimal(0);

    for (const id of cascadeIds) {
      const usedCredit = await this.recalculateOneUsedCredit(id, client);
      if (id === agentId) requestedUsedCredit = usedCredit;
    }

    return requestedUsedCredit;
  }

  private async recalculateOneUsedCredit(agentId: bigint, client: PrismaClientLike) {
    const directPlayers = await client.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 client.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 client.agentProfile.update({
      where: { userId: agentId },
      data: {
        usedCredit,
        directPlayerLiability: directLiability,
        childAgentExposure: childExposure,
      },
    });

    return usedCredit;
  }

  private async getCreditCascadeIds(agentId: bigint, client: PrismaClientLike) {
    const closureRows = await client.agentClosure.findMany({
      where: { descendantId: agentId },
      select: { ancestorId: true, depth: true },
      orderBy: { depth: 'asc' },
    });
    if (closureRows.length > 0) {
      return closureRows.map((row) => row.ancestorId);
    }

    const ids: bigint[] = [];
    let current: bigint | null = agentId;
    while (current) {
      ids.push(current);
      const profile: { parentAgentId: bigint | null } | null = await client.agentProfile.findUnique({
        where: { userId: current },
        select: { parentAgentId: true },
      });
      current = profile?.parentAgentId ?? null;
    }
    return ids;
  }

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

    const creditBefore = profile.creditLimit;
    const creditAfter = creditBefore.add(amt);
    if (creditAfter.lt(0)) throw appBadRequest('CREDIT_LIMIT_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,
        },
      });
    });

    await this.recalculateUsedCredit(agentId);

    return { creditAfter };
  }

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

  async adminDepositToPlayer(
    playerId: bigint,
    amount: number,
    operatorId: bigint,
    remark?: string,
    requestId?: string,
  ) {
    await this.assertPlayerParentCreditForDeposit(playerId, amount);
    const result = await this.funds.deposit({
      userId: playerId,
      amount,
      operatorId,
      remark: remark ?? '管理员上分',
      referenceId: requestId,
      transactionType: 'ADMIN_DEPOSIT',
      businessKey: requestId ? `admin-deposit:${operatorId}:${requestId}` : undefined,
    });
    const player = await this.prisma.user.findUnique({
      where: { id: playerId },
      select: { parentId: true },
    });
    if (player?.parentId) {
      await this.recalculateUsedCredit(player.parentId);
    }
    return result;
  }

  async adminWithdrawFromPlayer(
    playerId: bigint,
    amount: number,
    operatorId: bigint,
    remark?: string,
    requestId?: string,
  ) {
    const result = await this.funds.withdraw({
      userId: playerId,
      amount,
      operatorId,
      remark: remark ?? '管理员下分',
      referenceId: requestId,
      transactionType: 'ADMIN_WITHDRAW',
      businessKey: requestId ? `admin-withdraw:${operatorId}:${requestId}` : undefined,
    });
    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 appNotFound('PLAYER_NOT_FOUND');

    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: { in: [...AGENT_DAILY_DEPOSIT_TX_TYPES] },
            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,
    };
  }

  async depositToPlayer(
    agentId: bigint,
    playerId: bigint,
    amount: number,
    requestId: string,
    remark?: string,
  ) {
    const amt = new Decimal(amount);
    const businessKey = `agent-deposit:${agentId}:${requestId}`;

    await this.prisma.$transaction(async (tx) => {
      const existing = await tx.walletTransaction.findUnique({ where: { businessKey } });
      if (existing) return;

      await this.requireDirectPlayer(agentId, playerId, tx);
      await this.recalculateUsedCredit(agentId, tx);

      const profile = await this.lockAgentProfile(tx, agentId);
      const available = new Decimal(profile.credit_limit).sub(profile.used_credit);
      if (available.lt(amt)) {
        throw appBadRequest('INSUFFICIENT_AGENT_CREDIT');
      }

      await this.assertAgentDepositLimits(agentId, amt, tx);

      await this.funds.deposit({
        userId: playerId,
        amount: amt,
        operatorId: agentId,
        remark: remark ?? '代理上分',
        referenceId: requestId,
        transactionType: 'AGENT_DEPOSIT',
        businessKey,
        tx,
      });
      await this.recalculateUsedCredit(agentId, tx);
    });

    return { success: true };
  }

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

    await this.funds.withdraw({
      userId: playerId,
      amount,
      operatorId: agentId,
      remark: remark ?? '代理下分',
      referenceId: requestId,
      transactionType: 'AGENT_WITHDRAW',
      businessKey: `agent-withdraw:${agentId}:${requestId}`,
    });
    await this.recalculateUsedCredit(agentId);

    return { success: true };
  }

  private async requireDirectPlayer(agentId: bigint, playerId: bigint, tx?: TxClient) {
    const client = tx ?? this.prisma;
    const player = await client.user.findFirst({
      where: { id: playerId, userType: 'PLAYER', deletedAt: null },
      include: { auth: true, wallet: true, preferences: true },
    });
    if (!player) throw appNotFound('PLAYER_NOT_FOUND');
    if (player.parentId !== agentId) {
      throw appForbidden('MANAGE_DIRECT_PLAYERS_ONLY');
    }
    return player;
  }

  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 async assertAgentDepositLimits(
    creditAgentId: bigint,
    amount: Decimal,
    tx?: TxClient,
  ) {
    const client = tx ?? this.prisma;
    const profile = await client.agentProfile.findUnique({
      where: { userId: creditAgentId },
    });
    if (!profile) return;

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

    if (maxSingleDeposit && amount.gt(maxSingleDeposit)) {
      throw appBadRequest('AGENT_SINGLE_TOPUP_LIMIT');
    }

    if (maxDailyDeposit) {
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      const dailyAgg = await client.walletTransaction.aggregate({
        where: {
          operatorId: creditAgentId,
          transactionType: { in: [...AGENT_DAILY_DEPOSIT_TX_TYPES] },
          createdAt: { gte: today },
        },
        _sum: { amount: true },
      });
      const dailyTotal = new Decimal(dailyAgg._sum.amount ?? 0).add(amount);
      if (dailyTotal.gt(maxDailyDeposit)) {
        throw appBadRequest('AGENT_DAILY_TOPUP_LIMIT');
      }
    }
  }

  private async lockAgentProfile(tx: TxClient, agentId: bigint) {
    const profiles = await tx.$queryRaw<
      Array<{
        user_id: bigint;
        parent_agent_id: bigint | null;
        credit_limit: Decimal;
        used_credit: Decimal;
        max_single_deposit: Decimal | null;
        max_daily_deposit: Decimal | null;
      }>
    >`
      SELECT user_id, parent_agent_id, credit_limit, used_credit, max_single_deposit, max_daily_deposit
      FROM agent_profiles
      WHERE user_id = ${agentId}
      FOR UPDATE
    `;
    if (!profiles.length) throw appBadRequest('AGENT_PROFILE_NOT_FOUND');
    return profiles[0];
  }

  private async assertChildCreditWithinParent(
    parentAgentId: bigint,
    childProfile: { userId: bigint; creditLimit: Decimal; usedCredit: Decimal },
    creditAfter: Decimal,
  ) {
    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 (creditAfter.lt(0)) throw appBadRequest('CREDIT_LIMIT_NEGATIVE');
    if (creditAfter.gt(parent.creditLimit)) throw appBadRequest('CREDIT_EXCEEDS_PARENT');
    if (exposureDelta.gt(0) && exposureDelta.gt(parentAvailable)) {
      throw appBadRequest('INSUFFICIENT_AGENT_CREDIT');
    }
  }
}
