import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { Bill } from '../entities/bill.entity';
import { Student } from '../entities/student.entity';
import { StudentWallet } from '../entities/student-wallet.entity';
import { WalletTransaction } from '../entities/wallet-transaction.entity';
import { In } from 'typeorm';
import { BatchChangeWalletBalanceDto, ChangeWalletBalanceDto } from './dto/wallet.dto';
import { FinancialOperationsService } from '../financial-operations/financial-operations.service';
import { Room } from '../entities/room.entity';

const money = (value: number | string | null | undefined) => Number(Number(value || 0).toFixed(2));

@Injectable()
export class WalletsService {
  constructor(
    @InjectRepository(StudentWallet) private walletRepo: Repository<StudentWallet>,
    @InjectRepository(WalletTransaction) private transactionRepo: Repository<WalletTransaction>,
    @InjectRepository(Student) private studentRepo: Repository<Student>,
    private dataSource: DataSource,
    private financialOperations?: FinancialOperationsService,
  ) {}

  async findAll(query?: { keyword?: string; debtOnly?: boolean; roomType?: string }) {
    const qb = this.studentRepo
      .createQueryBuilder('student')
      .leftJoin('student.occupancies', 'occupancy', 'occupancy.checkOutDate IS NULL')
      .leftJoin('occupancy.room', 'room')
      .where('student.status = :status', { status: 'active' });

    if (query?.keyword) {
      qb.andWhere('(student.name LIKE :keyword OR student.studentNo LIKE :keyword)', {
        keyword: `%${query.keyword}%`,
      });
    }
    if (query?.roomType) {
      qb.andWhere('room.roomType = :roomType', { roomType: query.roomType });
    }

    const rows = await qb
      .select([
        'student.id AS studentId',
        'student.name AS studentName',
        'student.studentNo AS studentNo',
        'room.roomType AS roomType',
        'room.roomNumber AS roomNumber',
      ])
      .orderBy('student.name', 'ASC')
      .getRawMany<{
        studentId: number;
        studentName: string;
        studentNo: string | null;
        roomType: string | null;
        roomNumber: string | null;
      }>();
    if (!rows.length) return [];

    const ids = rows.map((row) => Number(row.studentId));
    const wallets = await this.walletRepo.find({ where: { studentId: In(ids) } });
    const bills = await this.dataSource.getRepository(Bill)
      .createQueryBuilder('bill')
      .select('bill.studentId', 'studentId')
      .addSelect('SUM(bill.outstandingAmount)', 'outstandingAmount')
      .where('bill.studentId IN (:...ids)', { ids })
      .andWhere('bill.status IN (:...statuses)', { statuses: ['unpaid', 'partially_paid'] })
      .groupBy('bill.studentId')
      .getRawMany<{ studentId: number; outstandingAmount: string }>();
    const walletMap = new Map(wallets.map((wallet) => [wallet.studentId, wallet]));
    const debtMap = new Map(bills.map((bill) => [Number(bill.studentId), money(bill.outstandingAmount)]));
    return rows
      .map((row) => ({
        studentId: Number(row.studentId),
        studentName: row.studentName,
        studentNo: row.studentNo || undefined,
        roomType: row.roomType || undefined,
        roomNumber: row.roomNumber || undefined,
        balance: money(walletMap.get(Number(row.studentId))?.balance),
        outstandingAmount: debtMap.get(Number(row.studentId)) || 0,
      }))
      .filter((row) => !query?.debtOnly || row.outstandingAmount > 0);
  }

  async findRoomTypes() {
    const rows = await this.dataSource
      .getRepository(Room)
      .createQueryBuilder('room')
      .innerJoin('room.occupancies', 'occupancy', 'occupancy.checkOutDate IS NULL')
      .innerJoin('occupancy.student', 'student', 'student.status = :status', { status: 'active' })
      .select('room.roomType', 'roomType')
      .where('room.roomType IS NOT NULL')
      .andWhere("room.roomType <> ''")
      .distinct(true)
      .orderBy('room.roomType', 'ASC')
      .getRawMany<{ roomType: string }>();
    return rows.map((row) => row.roomType);
  }

  async findTransactions(studentId: number) {
    return this.transactionRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } });
  }

  async changeBalance(dto: ChangeWalletBalanceDto, recordedBy?: number) {
    const { operationId, ...change } = dto;
    return this.financialOperations
      ? this.financialOperations.run(operationId, 'wallet.change_balance', () => this.changeBalanceOnce(change, recordedBy, operationId))
      : this.changeBalanceOnce(change, recordedBy, operationId);
  }

  private async changeBalanceOnce(
    dto: Omit<ChangeWalletBalanceDto, 'operationId'>,
    recordedBy?: number,
    operationId?: string,
    transactionManager?: EntityManager,
  ) {
    const amount = money(dto.amount);
    if (!Number.isFinite(dto.amount) || Math.abs(dto.amount * 100 - Math.round(dto.amount * 100)) > 1e-8) {
      throw new BadRequestException('调账金额最多保留两位小数');
    }
    if (amount === 0) throw new BadRequestException('调账金额不能为 0');
    if (dto.type === 'recharge' && amount <= 0) throw new BadRequestException('充值金额必须大于 0');
    const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
    if (!student) throw new NotFoundException('学生不存在');
    const work = async (manager: EntityManager) => {
      const wallet = await this.getOrCreateWallet(manager, dto.studentId, true);
      const nextBalance = money(Number(wallet.balance) + amount);
      if (nextBalance < 0) throw new BadRequestException('调账后余额不能小于 0');
      wallet.balance = nextBalance;
      await manager.save(wallet);
      await manager.save(
        manager.create(WalletTransaction, {
          studentId: dto.studentId,
          billId: null,
          operationId: operationId ?? null,
          type: dto.type,
          amount,
          balanceAfter: nextBalance,
          description: dto.description || (dto.type === 'recharge' ? '财务充值' : '余额调账'),
          recordedBy: recordedBy || null,
        }),
      );
      const payments = amount > 0 ? await this.settleOutstandingBills(manager, dto.studentId, recordedBy) : [];
      const finalWallet = await manager.findOneByOrFail(StudentWallet, { studentId: dto.studentId });
      return { wallet: finalWallet, payments };
    };
    return transactionManager ? work(transactionManager) : this.dataSource.transaction(work);
  }

  async batchChangeBalance(dto: BatchChangeWalletBalanceDto, recordedBy?: number) {
    const { operationId, ...batch } = dto;
    const work = () => this.dataSource.transaction(async (manager) => {
      const uniqueStudentIds = Array.from(new Set(batch.studentIds));
      const results: Array<{ wallet: StudentWallet; payments: Bill[] }> = [];
      for (const studentId of uniqueStudentIds) {
        results.push(await this.changeBalanceOnce({
          studentId,
          amount: batch.amount,
          type: batch.type,
          description: batch.description,
        }, recordedBy, operationId ? `${operationId}:${studentId}` : undefined, manager));
      }
      return { count: uniqueStudentIds.length, results };
    });
    return this.financialOperations
      ? this.financialOperations.run(operationId, 'wallet.batch_change_balance', work)
      : work();
  }

  async debitBill(manager: EntityManager, bill: Bill, recordedBy?: number) {
    if (bill.status === 'cancelled') return bill;

    const total = money(bill.totalAmount);
    const paid = Math.max(0, Math.min(money(bill.paidAmount), total));
    const remaining = money(Math.max(0, total - paid));
    if (remaining <= 0) {
      bill.paidAmount = total;
      bill.outstandingAmount = 0;
      bill.status = 'paid';
      return manager.save(bill);
    }

    const wallet = await this.getOrCreateWallet(manager, bill.studentId);
    const amount = money(Math.min(Math.max(0, money(wallet.balance)), remaining));
    if (amount <= 0) {
      bill.paidAmount = paid;
      bill.outstandingAmount = remaining;
      bill.status = paid > 0 ? 'partially_paid' : 'unpaid';
      return manager.save(bill);
    }

    wallet.balance = money(Number(wallet.balance) - amount);
    bill.paidAmount = money(paid + amount);
    bill.outstandingAmount = money(Math.max(0, total - Number(bill.paidAmount)));
    bill.status = bill.outstandingAmount <= 0 ? 'paid' : 'partially_paid';
    await manager.save(wallet);
    await manager.save(bill);
    await manager.save(
      manager.create(WalletTransaction, {
        studentId: bill.studentId,
        billId: bill.id,
        type: 'bill_payment',
        amount: -amount,
        balanceAfter: wallet.balance,
        description: `账单 #${bill.id} 自动扣款`,
        recordedBy: recordedBy ?? null,
      }),
    );
    return bill;
  }

  async refundBill(manager: EntityManager, bill: Bill, reason: string, recordedBy?: number) {
    if (bill.status === 'cancelled') return bill;

    const paid = Math.max(0, Math.min(money(bill.paidAmount), money(bill.totalAmount)));
    if (paid > 0) {
      const wallet = await this.getOrCreateWallet(manager, bill.studentId);
      wallet.balance = money(Number(wallet.balance) + paid);
      await manager.save(wallet);
      await manager.save(
        manager.create(WalletTransaction, {
          studentId: bill.studentId,
          billId: bill.id,
          type: 'bill_refund',
          amount: paid,
          balanceAfter: wallet.balance,
          description: `取消账单 #${bill.id} 冲正：${reason}`,
          recordedBy: recordedBy ?? null,
        }),
      );
    }
    bill.status = 'cancelled';
    bill.paidAmount = 0;
    bill.outstandingAmount = 0;
    bill.cancelledAt = new Date();
    bill.cancelReason = reason;
    return manager.save(bill);
  }

  private async settleOutstandingBills(manager: EntityManager, studentId: number, recordedBy?: number) {
    const bills = await manager
      .createQueryBuilder(Bill, 'bill')
      .where('bill.studentId = :studentId', { studentId })
      .andWhere('bill.status IN (:...statuses)', { statuses: ['unpaid', 'partially_paid'] })
      .andWhere('bill.outstandingAmount > 0')
      .orderBy('bill.periodStart', 'ASC')
      .addOrderBy('bill.id', 'ASC')
      .getMany();
    const settled: Bill[] = [];
    for (const bill of bills) {
      const wallet = await manager.findOne(StudentWallet, { where: { studentId } });
      if (!wallet || money(wallet.balance) <= 0) break;
      settled.push(await this.debitBill(manager, bill, recordedBy));
    }
    return settled;
  }

  private async getOrCreateWallet(manager: EntityManager, studentId: number, lock = false) {
    const find = async () => {
      if (!lock || !manager.createQueryBuilder) {
        return manager.findOne(StudentWallet, { where: { studentId } });
      }
      let query = manager
        .createQueryBuilder(StudentWallet, 'wallet')
        .where('wallet.studentId = :studentId', { studentId });
      if (['mysql', 'mariadb', 'postgres', 'cockroachdb'].includes(this.dataSource.options.type)) {
        query = query.setLock('pessimistic_write');
      }
      return query.getOne();
    };
    let wallet = await find();
    if (!wallet) {
      try {
        if (manager.insert) await manager.insert(StudentWallet, { studentId, balance: 0 });
        else wallet = await manager.save(manager.create(StudentWallet, { studentId, balance: 0 }));
      } catch {
        // A concurrent request may have inserted the one wallet row.
      }
      wallet ||= await find();
    }
    if (!wallet) throw new NotFoundException('学生钱包创建失败');
    return wallet;
  }
}
