import { BadRequestException, Injectable, NotFoundException, Optional } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, DataSource } from 'typeorm';
import { Bill } from '../entities/bill.entity';
import { BillItem } from '../entities/bill-item.entity';
import { RoomExpense } from '../entities/room-expense.entity';
import { PersonalExpense } from '../entities/personal-expense.entity';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { StudentWallet } from '../entities/student-wallet.entity';
import { CancelBillDto, GenerateBillsDto, UpdateBillStatusDto } from './dto/bill.dto';
import { WalletsService } from '../wallets/wallets.service';
import { FinancialOperationsService } from '../financial-operations/financial-operations.service';
import { BillsGenerationService } from './bills-generation.service';

interface AgentBillRow {
  billId: string | number;
  studentName: string;
  periodStart: string;
  periodEnd: string;
  totalAmount: string | number;
  paidAmount: string | number;
  outstandingAmount: string | number;
  status: string;
}

@Injectable()
export class BillsService {
  constructor(
    @InjectRepository(Bill) private billRepo: Repository<Bill>,
    @InjectRepository(BillItem) private itemRepo: Repository<BillItem>,
    @InjectRepository(RoomExpense) private roomExpRepo: Repository<RoomExpense>,
    @InjectRepository(PersonalExpense) private personalExpRepo: Repository<PersonalExpense>,
    @InjectRepository(Occupancy) private occRepo: Repository<Occupancy>,
    @InjectRepository(Room) private roomRepo: Repository<Room>,
    private dataSource: DataSource,
    private walletsService: WalletsService,
    private generation: BillsGenerationService,
    @Optional()
    private financialOperations?: FinancialOperationsService,
  ) {}

  /**
   * 核心计费引擎：按"人天数"加权分摊
   */
  async generateBills(dto: GenerateBillsDto) {
    const { operationId, ...request } = dto;
    const work = () => this.generation.generateBillsOnce(request);
    return this.financialOperations
      ? this.financialOperations.run(operationId, 'bill.generate', work)
      : work();
  }

  async createImmediatePersonalBill(
    expense: PersonalExpense,
    periodStart: string,
    periodEnd: string,
    recordedBy?: number,
  ) {
    return this.dataSource.transaction(async (manager) => {
      expense = await manager.save(manager.create(PersonalExpense, expense));
      let bill = await manager.save(
        manager.create(Bill, {
          studentId: expense.studentId,
          periodStart,
          periodEnd,
          sharedAmount: 0,
          personalAmount: Number(expense.amount),
          totalAmount: Number(expense.amount),
          source: 'student_utility',
          paidAmount: 0,
          outstandingAmount: Number(expense.amount),
          status: 'unpaid',
        }),
      );
      await manager.save(
        manager.create(BillItem, {
          billId: bill.id,
          personalExpenseId: expense.id,
          roomId: expense.roomId,
          expenseType: expense.expenseType,
          description:
            expense.description || (expense.expenseType === 'water' ? '学生水费' : '学生电费'),
          days: 0,
          totalRoomDays: 0,
          roomTotalAmount: expense.amount,
          studentAmount: expense.amount,
        }),
      );
      expense.billId = bill.id;
      await manager.save(expense);
      bill = await this.walletsService.debitBill(manager, bill, recordedBy);
      return { expense, bill };
    });
  }

  async findAll(query?: {
    periodStart?: string;
    periodEnd?: string;
    studentId?: number;
    status?: string;
    expenseType?: string;
  }) {
    const qb = this.billRepo
      .createQueryBuilder('b')
      .leftJoinAndSelect('b.student', 'student')
      .orderBy('b.generatedAt', 'DESC');
    if (query?.periodStart) qb.andWhere('b.periodStart = :ps', { ps: query.periodStart });
    if (query?.periodEnd) qb.andWhere('b.periodEnd = :pe', { pe: query.periodEnd });
    if (query?.studentId) qb.andWhere('b.studentId = :sid', { sid: query.studentId });
    if (query?.status) qb.andWhere('b.status = :status', { status: query.status });
    if (query?.expenseType) {
      qb.innerJoin('b.items', 'bi', 'bi.expenseType = :et', { et: query.expenseType });
    }
    const bills = await qb.getMany();
    return this.attachDepositInfo(bills);
  }

  async agentSearchBills(query: {
    keyword?: string;
    periodStart?: string;
    periodEnd?: string;
    status?: string;
    limit?: number;
  }) {
    const billSelects = [
      ['student.name', 'studentName'],
      ['bill.periodStart', 'periodStart'],
      ['bill.periodEnd', 'periodEnd'],
      ['bill.totalAmount', 'totalAmount'],
      ['bill.paidAmount', 'paidAmount'],
      ['bill.outstandingAmount', 'outstandingAmount'],
      ['bill.status', 'status'],
    ] as const;
    const qb = this.billRepo
      .createQueryBuilder('bill')
      .leftJoin('bill.student', 'student')
      .select('bill.id', 'billId');
    for (const [column, alias] of billSelects) {
      qb.addSelect(column, alias);
    }
    if (query.keyword) {
      const billId = Number(query.keyword);
      if (Number.isInteger(billId) && billId > 0) {
        qb.andWhere('(student.name LIKE :keyword OR bill.id = :billId)', {
          keyword: `%${query.keyword}%`,
          billId,
        });
      } else {
        qb.andWhere('student.name LIKE :keyword', { keyword: `%${query.keyword}%` });
      }
    }
    if (query.periodStart)
      qb.andWhere('bill.periodStart >= :periodStart', { periodStart: query.periodStart });
    if (query.periodEnd)
      qb.andWhere('bill.periodEnd <= :periodEnd', { periodEnd: query.periodEnd });
    if (query.status) qb.andWhere('bill.status = :status', { status: query.status });
    const rows = await qb
      .orderBy('bill.generatedAt', 'DESC')
      .limit(query.limit ?? 20)
      .getRawMany<AgentBillRow>();
    return rows.map((row) => ({
      ...row,
      billId: Number(row.billId),
      totalAmount: Number(row.totalAmount || 0),
      paidAmount: Number(row.paidAmount || 0),
      outstandingAmount: Number(row.outstandingAmount || 0),
    }));
  }

  async findOne(id: number) {
    const bill = await this.billRepo.findOne({ where: { id }, relations: ['student', 'items'] });
    if (!bill) throw new NotFoundException('账单不存在');
    const [withDeposit] = await this.attachDepositInfo([bill]);
    return withDeposit;
  }

  /** 查询时附加钱包余额和实际支付数据。 */
  private async attachDepositInfo(bills: Bill[]): Promise<any[]> {
    if (!bills?.length) return bills;
    const studentIds = Array.from(new Set(bills.map((bill) => bill.studentId)));
    const wallets = await this.dataSource
      .getRepository(StudentWallet)
      .createQueryBuilder('wallet')
      .where('wallet.studentId IN (:...ids)', { ids: studentIds })
      .getMany();
    const balanceMap = new Map(
      wallets.map((wallet: any) => [wallet.studentId, Number(wallet.balance || 0)]),
    );
    return bills.map((bill) => ({
      ...bill,
      walletBalance: Number((balanceMap.get(bill.studentId) || 0).toFixed(2)),
      paidAmount: Number(bill.paidAmount || 0),
      outstandingAmount: Number(bill.outstandingAmount || 0),
    }));
  }

  async updateStatus(id: number, dto: UpdateBillStatusDto) {
    const bill = await this.billRepo.findOne({ where: { id } });
    if (!bill) throw new NotFoundException('账单不存在');
    this.assertStatusMatchesAmounts(bill, dto.status);
    bill.status = dto.status;
    return this.billRepo.save(bill);
  }

  async batchUpdateStatus(ids: number[], status: string) {
    const uniqueIds = [...new Set(ids || [])];
    if (uniqueIds.length === 0) throw new BadRequestException('请选择要更新的账单');
    if (!['unpaid', 'partially_paid', 'paid'].includes(status))
      throw new BadRequestException('账单状态无效');
    const bills = await this.billRepo.find({ where: { id: In(uniqueIds) } });
    if (bills.length !== uniqueIds.length) throw new NotFoundException('部分账单不存在');
    for (const bill of bills) this.assertStatusMatchesAmounts(bill, status);
    await this.billRepo
      .createQueryBuilder()
      .update()
      .set({ status })
      .where('id IN (:...ids)', { ids: uniqueIds })
      .execute();
    return { message: `成功更新 ${uniqueIds.length} 条账单状态` };
  }

  async cancel(id: number, dto: CancelBillDto, recordedBy?: number) {
    const reason = dto.reason?.trim();
    if (!reason) throw new BadRequestException('取消原因不能为空');
    const work = () =>
      this.dataSource.transaction(async (manager) => {
        const bill = await manager
          .createQueryBuilder(Bill, 'bill')
          .where('bill.id = :id', { id })
          .setLock('pessimistic_write')
          .getOne();
        if (!bill) throw new NotFoundException('账单不存在');
        if (bill.status === 'cancelled') throw new BadRequestException('账单已经取消');
        await manager.update(PersonalExpense, { billId: id }, { billId: null });
        return this.walletsService.refundBill(manager, bill, reason, recordedBy);
      });
    return this.financialOperations
      ? this.financialOperations.run(dto.operationId, `bill.cancel:${id}`, work)
      : work();
  }

  async remove(id: number) {
    const exists = await this.billRepo.findOne({ where: { id } });
    if (!exists) throw new NotFoundException('账单不存在');
    if (exists.status === 'cancelled') throw new BadRequestException('账单已归档');
    if (Number(exists.paidAmount) > 0) {
      throw new BadRequestException('已发生资金流水的账单请使用取消账单并冲正');
    }
    await this.billRepo.update(id, {
      status: 'cancelled',
      outstandingAmount: 0,
      cancelReason: '归档未支付账单',
      cancelledAt: new Date(),
    });
    return { message: '账单已归档' };
  }

  async purge(id: number) {
    const bill = await this.billRepo.findOne({ where: { id } });
    if (!bill) throw new NotFoundException('账单不存在');
    if (bill.status !== 'cancelled') {
      throw new BadRequestException('仅已取消账单可以永久删除，请先取消账单');
    }
    if (Number(bill.paidAmount) > 0) {
      throw new BadRequestException('已发生资金流水的账单不能永久删除');
    }
    const personalExpenseCount = await this.personalExpRepo.count({ where: { billId: id } });
    if (personalExpenseCount > 0) {
      throw new BadRequestException('该账单仍关联个人费用，无法永久删除');
    }
    await this.dataSource.transaction(async (manager) => {
      await manager.delete(BillItem, { billId: id });
      await manager.delete(Bill, id);
    });
    return { message: '已永久删除账单（不可恢复）' };
  }

  async batchPurge(ids: number[]) {
    const uniqueIds = [...new Set(ids || [])];
    if (uniqueIds.length === 0) throw new BadRequestException('请选择要永久删除的账单');
    if (uniqueIds.some((id) => !Number.isInteger(id) || id <= 0)) {
      throw new BadRequestException('账单 ID 无效');
    }
    const bills = await this.billRepo.find({ where: { id: In(uniqueIds) } });
    if (bills.length !== uniqueIds.length) throw new NotFoundException('部分账单不存在');
    const personalExpenseCount = await this.personalExpRepo.count({
      where: { billId: In(uniqueIds) },
    });
    if (personalExpenseCount > 0) {
      throw new BadRequestException('选中账单仍关联个人费用，无法永久删除');
    }

    const deleted: number[] = [];
    const skipped: string[] = [];
    for (const bill of bills) {
      if (bill.status !== 'cancelled') {
        skipped.push(`账单${bill.id}（未取消）`);
        continue;
      }
      if (Number(bill.paidAmount) > 0) {
        skipped.push(`账单${bill.id}（已支付）`);
        continue;
      }
      await this.dataSource.transaction(async (manager) => {
        await manager.delete(BillItem, { billId: bill.id });
        await manager.delete(Bill, bill.id);
      });
      deleted.push(bill.id);
    }
    const message =
      skipped.length > 0
        ? `已永久删除 ${deleted.length} 条账单；${skipped.length} 条被跳过（${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''}）`
        : `已永久删除 ${deleted.length} 条账单（不可恢复）`;
    return { message, deleted: deleted.length, skipped: skipped.length };
  }

  async batchRemove(ids: number[]) {
    const uniqueIds = [...new Set(ids || [])];
    if (uniqueIds.length === 0) throw new BadRequestException('请选择要归档的账单');
    const bills = await this.billRepo.find({ where: { id: In(uniqueIds) } });
    if (bills.length !== uniqueIds.length) throw new NotFoundException('部分账单不存在');
    if (bills.some((bill) => Number(bill.paidAmount) > 0)) {
      throw new BadRequestException('选中账单包含资金流水，请逐条取消并冲正');
    }
    const targetIds = bills.filter((bill) => bill.status !== 'cancelled').map((bill) => bill.id);
    if (targetIds.length > 0) {
      await this.billRepo
        .createQueryBuilder()
        .update()
        .set({
          status: 'cancelled',
          outstandingAmount: 0,
          cancelReason: '批量归档未支付账单',
          cancelledAt: new Date(),
        })
        .where('id IN (:...ids)', { ids: targetIds })
        .execute();
    }
    return { message: `成功归档 ${targetIds.length} 条账单`, archived: targetIds.length };
  }

  private assertStatusMatchesAmounts(bill: Bill, status: string) {
    const paid = Number(bill.paidAmount || 0);
    const outstanding = Number(bill.outstandingAmount || 0);
    const matches =
      status === 'paid'
        ? outstanding <= 0
        : status === 'partially_paid'
          ? paid > 0 && outstanding > 0
          : status === 'unpaid' && paid <= 0 && outstanding > 0;
    if (!matches) throw new BadRequestException('账单状态必须与实付及未付金额一致');
  }
}
