import { Injectable, BadRequestException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateTransactionId } from '../../shared/common/decorators';

type TxClient = Prisma.TransactionClient;

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

  async getWallet(userId: bigint) {
    const wallet = await this.prisma.wallet.findUnique({ where: { userId } });
    if (!wallet) throw new BadRequestException('Wallet not found');
    return wallet;
  }

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

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

  async deposit(
    userId: bigint,
    amount: Decimal | number,
    operatorId: bigint,
    remark?: string,
    referenceId?: string,
    transactionType = 'MANUAL_DEPOSIT',
  ) {
    const amt = new Decimal(amount);
    if (amt.lte(0)) throw new BadRequestException('Amount must be positive');

    return this.prisma.$transaction(async (tx) => {
      const w = await this.lockWallet(tx, userId);
      const balanceBefore = new Decimal(w.available_balance);
      const balanceAfter = balanceBefore.add(amt);

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

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

      return { balanceAfter };
    });
  }

  async withdraw(
    userId: bigint,
    amount: Decimal | number,
    operatorId: bigint,
    remark?: string,
    referenceId?: string,
  ) {
    const amt = new Decimal(amount);
    if (amt.lte(0)) throw new BadRequestException('Amount must be positive');

    return this.prisma.$transaction(async (tx) => {
      const w = await this.lockWallet(tx, userId);
      const balanceBefore = new Decimal(w.available_balance);
      if (balanceBefore.lt(amt)) throw new BadRequestException('Insufficient balance');
      const balanceAfter = balanceBefore.sub(amt);

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

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

      return { balanceAfter };
    });
  }

  async freezeForBet(userId: bigint, stake: Decimal | number, betId: string) {
    const amt = new Decimal(stake);

    return this.prisma.$transaction(async (tx) => {
      const w = await this.lockWallet(tx, userId);
      const avail = new Decimal(w.available_balance);
      if (avail.lt(amt)) throw new BadRequestException('Insufficient balance');

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

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

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

  async settleBet(
    userId: bigint,
    stake: Decimal,
    payout: Decimal,
    betId: string,
    result: 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE',
    tx?: TxClient,
  ) {
    const run = async (client: TxClient) => {
      const txTypeMap: Record<string, string> = {
        WIN: 'BET_SETTLE_WIN',
        LOSE: 'BET_SETTLE_LOSE',
        PUSH: 'BET_SETTLE_PUSH',
        VOID: 'BET_VOID_REFUND',
        HALF_WIN: 'BET_SETTLE_WIN',
        HALF_LOSE: 'BET_SETTLE_LOSE',
      };

      const w = await this.lockWallet(client, userId);
      const avail = new Decimal(w.available_balance);
      const frozen = new Decimal(w.frozen_balance);
      const frozenAfter = frozen.sub(stake);
      const balanceAfter = avail.add(payout);

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

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

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

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

    const run = async (client: TxClient) => {
      const w = await this.lockWallet(client, userId);
      const avail = new Decimal(w.available_balance);
      const balanceAfter = avail.add(delta);
      const txType = delta.gt(0) ? 'BET_SETTLE_WIN' : 'RESETTLE_REVERSE';

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

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

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

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

    return {
      transactionId: tx.transactionId,
      transactionType: tx.transactionType,
      amount: tx.amount.toString(),
      balanceBefore: tx.balanceBefore.toString(),
      balanceAfter: tx.balanceAfter.toString(),
      frozenBefore: tx.frozenBefore.toString(),
      frozenAfter: tx.frozenAfter.toString(),
      referenceType: tx.referenceType,
      referenceId: tx.referenceId,
      remark: tx.remark,
      createdAt: tx.createdAt.toISOString(),
      betNo: tx.referenceType === 'BET' ? tx.referenceId : null,
    };
  }

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

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

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

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

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