import { BadRequestException, Injectable, NotFoundException, Optional } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, DataSource, EntityManager } 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';


@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,
    @Optional()
    private financialOperations?: FinancialOperationsService,
  ) {}

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

  private async generateBillsOnce(dto: GenerateBillsDto) {
    const { periodStart, periodEnd } = dto.billingMonth
      ? this.resolveBillingPeriod(dto.billingMonth)
      : { periodStart: dto.periodStart!, periodEnd: dto.periodEnd! };
    if (!this.isValidDate(periodStart) || !this.isValidDate(periodEnd) || periodEnd < periodStart) {
      throw new BadRequestException('账单周期无效，结束日期不能早于开始日期');
    }
    const pStart = new Date(`${periodStart}T00:00:00Z`);
    const pEnd = new Date(`${periodEnd}T00:00:00Z`);
    const existingBills = await this.billRepo.find({ where: { periodStart, periodEnd } });
    if (existingBills.length > 0) {
      throw new BadRequestException(`${dto.billingMonth || `${periodStart}~${periodEnd}`} 账单已生成，不能重复生成`);
    }
    const roomExpenses = await this.roomExpRepo
      .createQueryBuilder('e')
      .where('e.periodStart >= :periodStart AND e.periodEnd <= :periodEnd', { periodStart, periodEnd })
      .andWhere('e.status = :status', { status: 'active' })
      .getMany();
    const longTermOccupancies: Occupancy[] = [];
    const roomExpMap = new Map<number, RoomExpense[]>();
    for (const expense of roomExpenses) {
      const expenses = roomExpMap.get(expense.roomId) || [];
      expenses.push(expense);
      roomExpMap.set(expense.roomId, expenses);
    }
    const roomIds = new Set([
      ...roomExpMap.keys(),
      ...longTermOccupancies.filter((occupancy) => occupancy.stayType === 'long').map((occupancy) => occupancy.roomId),
    ]);
    const studentBillData = new Map<number, { shared: number; items: Array<Record<string, unknown>> }>();

    for (const roomId of roomIds) {
      const expenses = roomExpMap.get(roomId) || [];
      const occupancies = await this.occRepo
        .createQueryBuilder('o')
        .leftJoinAndSelect('o.student', 'student')
        .leftJoinAndSelect('o.room', 'room')
        .where('o.roomId = :roomId', { roomId })
        .andWhere('o.billingStartDate <= :periodEnd', { periodEnd })
        .andWhere('(o.billingEndDate IS NULL OR o.billingEndDate >= :periodStart)', { periodStart })
        .getMany();
      const shortTermOccs = occupancies.filter((occupancy) => occupancy.stayType !== 'long');
      const longTermOccs = occupancies.filter((occupancy) => occupancy.stayType === 'long');

      for (const occupancy of longTermOccs) {
        const rent = this.calculateLongTermRent(
          occupancy,
          periodStart,
          periodEnd,
          Number(occupancy.room?.monthlyRate || 0),
        );
        if (rent <= 0) continue;
        const data = studentBillData.get(occupancy.studentId) || { shared: 0, items: [] };
        data.shared += rent;
        data.items.push({
          roomId,
          expenseType: 'rent',
          description: `长租月租费 (${occupancy.room?.roomNumber || '未知房间'})`,
          days: 0,
          totalRoomDays: 0,
          roomTotalAmount: rent,
          studentAmount: rent,
        });
        studentBillData.set(occupancy.studentId, data);
      }

      const studentDays = shortTermOccs.map((occupancy) => {
        const start = new Date(Math.max(new Date(occupancy.billingStartDate).getTime(), pStart.getTime()));
        const end = occupancy.billingEndDate
          ? new Date(Math.min(new Date(occupancy.billingEndDate).getTime(), pEnd.getTime()))
          : pEnd;
        const days = Math.max(0, Math.ceil((end.getTime() - start.getTime()) / 86_400_000) + 1);
        return { studentId: occupancy.studentId, days };
      });
      const totalDays = studentDays.reduce((sum, entry) => sum + entry.days, 0);
      if (totalDays === 0) continue;

      for (const expense of expenses) {
        const eligibleDays = studentDays.filter((entry) => entry.days > 0);
        const expenseTotal = Number(Number(expense.amount).toFixed(2));
        let allocated = 0;
        for (const [index, entry] of eligibleDays.entries()) {
          const amount = index === eligibleDays.length - 1
            ? Number((expenseTotal - allocated).toFixed(2))
            : Number(((entry.days / totalDays) * expenseTotal).toFixed(2));
          allocated = Number((allocated + amount).toFixed(2));
          const data = studentBillData.get(entry.studentId) || { shared: 0, items: [] };
          data.shared += amount;
          data.items.push({
            roomExpenseId: expense.id,
            roomId,
            expenseType: expense.expenseType,
            description: `${expense.expenseType} 分摊`,
            days: entry.days,
            totalRoomDays: totalDays,
            roomTotalAmount: expense.amount,
            studentAmount: amount,
          });
          studentBillData.set(entry.studentId, data);
        }
      }
    }

    const personalExps = await this.personalExpRepo
      .createQueryBuilder('pe')
      .where('pe.expenseDate >= :periodStart AND pe.expenseDate <= :periodEnd', { periodStart, periodEnd })
      .andWhere('pe.status = :status', { status: 'active' })
      .andWhere('pe.billId IS NULL')
      .getMany();
    const personalMap = new Map<number, number>();
    const personalItems = new Map<number, Array<Record<string, unknown>>>();
    for (const expense of personalExps) {
      personalMap.set(expense.studentId, (personalMap.get(expense.studentId) || 0) + Number(expense.amount));
      const items = personalItems.get(expense.studentId) || [];
      items.push({
        personalExpenseId: expense.id,
        roomId: expense.roomId,
        expenseType: expense.expenseType,
        description: `个人费用: ${expense.description || expense.expenseType}`,
        days: 0,
        totalRoomDays: 0,
        roomTotalAmount: expense.amount,
        studentAmount: expense.amount,
      });
      personalItems.set(expense.studentId, items);
    }

    const allStudentIds = new Set([...studentBillData.keys(), ...personalMap.keys()]);
    const bills = await this.dataSource.transaction(async (manager) => {
      const generated: Bill[] = [];
      for (const studentId of allStudentIds) {
        const shared = studentBillData.get(studentId)?.shared || 0;
        const personal = personalMap.get(studentId) || 0;
        const total = Number((shared + personal).toFixed(2));
        let bill = await manager.save(manager.create(Bill, {
          studentId,
          periodStart,
          periodEnd,
          sharedAmount: Number(shared.toFixed(2)),
          personalAmount: personal,
          totalAmount: total,
          source: 'batch',
          paidAmount: 0,
          outstandingAmount: total,
          status: 'unpaid',
        }));
        const items = [...(studentBillData.get(studentId)?.items || []), ...(personalItems.get(studentId) || [])];
        for (const item of items) await manager.save(manager.create(BillItem, { ...item, billId: bill.id }));
        const includedPersonal = personalExps.filter((expense) => expense.studentId === studentId);
        if (includedPersonal.length) {
          await manager.createQueryBuilder()
            .update(PersonalExpense)
            .set({ billId: bill.id })
            .where('id IN (:...ids)', { ids: includedPersonal.map((expense) => expense.id) })
            .execute();
        }
        bill = await this.walletsService.debitBill(manager, bill);
        generated.push(bill);
      }
      return generated;
    });
    return { message: `成功生成 ${bills.length} 条账单`, count: bills.length, bills, periodStart, periodEnd };
  }

  private calculateLongTermRent(occupancy: Occupancy, periodStart: string, periodEnd: string, monthlyRate: number) {
    const activeStart = occupancy.billingStartDate > periodStart ? occupancy.billingStartDate : periodStart;
    const activeEnd = occupancy.billingEndDate && occupancy.billingEndDate < periodEnd
      ? occupancy.billingEndDate
      : periodEnd;
    if (activeEnd < activeStart || monthlyRate <= 0) return 0;
    const [startYear, startMonth] = activeStart.split('-').map(Number);
    const [endYear, endMonth] = activeEnd.split('-').map(Number);
    let total = 0;
    for (let year = startYear, month = startMonth; year < endYear || (year === endYear && month <= endMonth);) {
      const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
      const prefix = `${year}-${String(month).padStart(2, '0')}-`;
      const overlapStart = activeStart > `${prefix}01` ? activeStart : `${prefix}01`;
      const monthEnd = `${prefix}${String(daysInMonth).padStart(2, '0')}`;
      const overlapEnd = activeEnd < monthEnd ? activeEnd : monthEnd;
      const days = Math.floor((Date.parse(`${overlapEnd}T00:00:00Z`) - Date.parse(`${overlapStart}T00:00:00Z`)) / 86_400_000) + 1;
      total += monthlyRate * days / daysInMonth;
      if (++month > 12) { month = 1; year++; }
    }
    return Number(total.toFixed(2));
  }

  private isValidDate(value: string) {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return false;
    const date = new Date(`${value}T00:00:00Z`);
    return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
  }

  private resolveBillingPeriod(billingMonth: string) {
    const matched = /^(\d{4})-(\d{2})$/.exec(billingMonth || '');
    if (!matched) throw new BadRequestException('账单月份格式错误，请使用 YYYY-MM');
    const year = Number(matched[1]);
    const month = Number(matched[2]);
    if (month < 1 || month > 12) throw new BadRequestException('账单月份格式错误，请使用 YYYY-MM');
    const targetMonthStart = new Date(year, month - 1, 1);
    const currentMonthStart = new Date();
    currentMonthStart.setDate(1);
    currentMonthStart.setHours(0, 0, 0, 0);
    if (targetMonthStart >= currentMonthStart) throw new BadRequestException('只能生成已结束月份的账单');
    const targetMonthEnd = new Date(year, month, 0);
    const pad = (value: number) => String(value).padStart(2, '0');
    return { periodStart: `${year}-${pad(month)}-01`, periodEnd: `${year}-${pad(month)}-${pad(targetMonthEnd.getDate())}` };
  }

  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 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 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('账单状态必须与实付及未付金额一致');
  }
}
