import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, In, Repository } from 'typeorm';
import { RoomExpense } from '../entities/room-expense.entity';
import { PersonalExpense } from '../entities/personal-expense.entity';
import { Room } from '../entities/room.entity';
import { Student } from '../entities/student.entity';
import {
  CreateRoomExpenseDto,
  CreatePersonalExpenseDto,
  BatchRoomExpenseDto,
  CreateStudentUtilityBillDto,
} from './dto/expense.dto';
import { BillsService } from '../bills/bills.service';
import { ExpenseOperationsService } from './expense-operations.service';


@Injectable()
export class ExpensesService {
  constructor(
    @InjectRepository(RoomExpense) private roomExpRepo: Repository<RoomExpense>,
    @InjectRepository(PersonalExpense) private personalExpRepo: Repository<PersonalExpense>,
    @InjectRepository(Room) private roomRepo: Repository<Room>,
    @InjectRepository(Student) private studentRepo: Repository<Student>,
    private billsService: BillsService,
    private dataSource: DataSource,
    private operations: ExpenseOperationsService,
  ) {}

  async getFormLookups() {
    const [rooms, students] = await Promise.all([
      this.roomRepo.find({
        select: ['id', 'roomNumber', 'building'],
        order: { building: 'ASC', roomNumber: 'ASC' },
      }),
      this.studentRepo.find({
        select: ['id', 'name', 'studentNo'],
        where: { status: 'active' },
        order: { name: 'ASC' },
      }),
    ]);
    return { rooms, students };
  }

  /** 费用录入/编辑表单需要的在读学生下拉项。 */
  async getStudentLookups() {
    return this.studentRepo.find({
      select: ['id', 'name', 'studentNo'],
      where: { status: 'active' },
      order: { name: 'ASC' },
    });
  }

  // 宿舍费用
  async createRoomExpense(dto: CreateRoomExpenseDto, userId?: number) {
    this.assertValidPeriod(dto.periodStart, dto.periodEnd);
    this.assertPositiveAmount(dto.amount);
    const room = await this.roomRepo.findOne({ where: { id: dto.roomId } });
    if (!room) throw new NotFoundException('宿舍不存在');
    const entity = this.roomExpRepo.create({ ...dto, recordedBy: userId });
    return this.roomExpRepo.save(entity);
  }

  async batchCreateRoomExpenses(dto: BatchRoomExpenseDto, userId?: number) {
    this.assertValidPeriod(dto.periodStart, dto.periodEnd);
    if (!dto.expenses?.length) throw new BadRequestException('请至少填写一条费用');
    dto.expenses.forEach((expense) => this.assertPositiveAmount(expense.amount));
    const roomIds = [...new Set(dto.expenses.map((expense) => expense.roomId))];
    const existingRooms = await this.roomRepo.find({ where: { id: In(roomIds) }, select: ['id'] });
    if (existingRooms.length !== roomIds.length) throw new NotFoundException('部分宿舍不存在');
    const entities = dto.expenses.map((e) => {
      const entity = this.roomExpRepo.create({
        roomId: e.roomId,
        expenseType: e.expenseType,
        amount: e.amount,
        description: e.description,
        periodStart: dto.periodStart,
        periodEnd: dto.periodEnd,
        recordedBy: userId,
      });
      return entity;
    });
    return this.roomExpRepo.save(entities);
  }

  async findRoomExpenses(query?: { roomId?: number; periodStart?: string; periodEnd?: string; status?: 'active' | 'archived' }) {
    const status = query?.status ?? 'active';
    if (status !== 'active' && status !== 'archived') throw new BadRequestException('费用状态无效');
    const qb = this.roomExpRepo
      .createQueryBuilder('e')
      .leftJoinAndSelect('e.room', 'room')
      .where('e.status = :status', { status })
      .orderBy('e.createdAt', 'DESC');
    if (query?.roomId) qb.andWhere('e.roomId = :roomId', { roomId: query.roomId });
    if (query?.periodStart) qb.andWhere('e.periodStart >= :ps', { ps: query.periodStart });
    if (query?.periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: query.periodEnd });
    return qb.getMany();
  }

  /**
   * Agent tool: 费用查询（宿舍费 + 个人附加费），返回白名单字段。
   */
  async agentSearchExpenses(query?: {
    keyword?: string;
    periodStart?: string;
    periodEnd?: string;
    limit?: number;
  }): Promise<{
    roomExpenses: {
      id: number;
      expenseType: string;
      amount: number;
      periodStart: string;
      periodEnd: string;
      roomNumber: string;
      status: string;
    }[];
    personalExpenses: {
      id: number;
      expenseType: string;
      amount: number;
      expenseDate: string;
      studentName: string;
      studentNo: string;
      status: string;
    }[];
  }> {
    const limit = Math.max(1, Math.min(query?.limit ?? 10, 30));

    const roomQb = this.roomExpRepo
      .createQueryBuilder('e')
      .leftJoin('e.room', 'room')
      .select('e.id', 'id');
    const roomExpenseSelects = [
      ['e.expenseType', 'expenseType'],
      ['e.amount', 'amount'],
      ['e.periodStart', 'periodStart'],
      ['e.periodEnd', 'periodEnd'],
      ['room.roomNumber', 'roomNumber'],
    ] as const;
    for (const [column, alias] of roomExpenseSelects) {
      roomQb.addSelect(column, alias);
    }
    roomQb.where('e.status = :status', { status: 'active' });
    if (query?.keyword) {
      roomQb.andWhere('room.roomNumber LIKE :keyword', { keyword: `%${query.keyword}%` });
    }
    if (query?.periodStart) {
      roomQb.andWhere('e.periodStart >= :periodStart', { periodStart: query.periodStart });
    }
    if (query?.periodEnd) {
      roomQb.andWhere('e.periodEnd <= :periodEnd', { periodEnd: query.periodEnd });
    }
    const roomRows = await roomQb
      .orderBy('e.createdAt', 'DESC')
      .limit(limit)
      .getRawMany<Record<string, unknown>>();

    const personalQb = this.personalExpRepo
      .createQueryBuilder('e')
      .leftJoin('e.student', 'student')
      .select('e.id', 'id');
    const personalExpenseSelects = [
      ['e.expenseType', 'expenseType'],
      ['e.amount', 'amount'],
      ['e.expenseDate', 'expenseDate'],
      ['student.name', 'studentName'],
      ['student.studentNo', 'studentNo'],
    ] as const;
    for (const [column, alias] of personalExpenseSelects) {
      personalQb.addSelect(column, alias);
    }
    personalQb.where('e.status = :status', { status: 'active' });
    if (query?.keyword) {
      personalQb.andWhere(
        '(student.name LIKE :keyword OR student.studentNo LIKE :keyword)',
        { keyword: `%${query.keyword}%` },
      );
    }
    if (query?.periodStart) {
      personalQb.andWhere('e.expenseDate >= :periodStart', { periodStart: query.periodStart });
    }
    if (query?.periodEnd) {
      personalQb.andWhere('e.expenseDate <= :periodEnd', { periodEnd: query.periodEnd });
    }
    const personalRows = await personalQb
      .orderBy('e.createdAt', 'DESC')
      .limit(limit)
      .getRawMany<Record<string, unknown>>();

    return {
      roomExpenses: roomRows.map((row) => ({
        id: Number(row.id),
        expenseType: String(row.expenseType),
        amount: Number(row.amount),
        periodStart: String(row.periodStart),
        periodEnd: String(row.periodEnd),
        roomNumber: row.roomNumber == null ? '' : String(row.roomNumber),
        status: String(row.status),
      })),
      personalExpenses: personalRows.map((row) => ({
        id: Number(row.id),
        expenseType: String(row.expenseType),
        amount: Number(row.amount),
        expenseDate: String(row.expenseDate),
        studentName: row.studentName == null ? '' : String(row.studentName),
        studentNo: row.studentNo == null ? '' : String(row.studentNo),
        status: String(row.status),
      })),
    };
  }

  async deleteRoomExpense(id: number) {
    const e = await this.roomExpRepo.findOne({ where: { id } });
    if (!e) throw new NotFoundException('费用记录不存在');
    const billed = await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: id } });
    if (billed) throw new BadRequestException('已计入账单的宿舍费用不能归档，请先取消账单');
    if (e.status === 'archived') throw new BadRequestException('费用记录已归档');
    await this.roomExpRepo.update(id, { status: 'archived' });
    return { message: '已归档' };
  }

  async batchDeleteRoomExpenses(ids: number[]) {
    const uniqueIds = [...new Set(ids || [])];
    if (uniqueIds.length === 0) throw new BadRequestException('请选择要归档的记录');
    const existing = await this.roomExpRepo.find({ where: { id: In(uniqueIds) }, select: ['id'] });
    const billed = await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: In(uniqueIds) } });
    if (billed) throw new BadRequestException('选中记录包含已计入账单的宿舍费用');
    if (existing.length !== uniqueIds.length) throw new NotFoundException('部分费用记录不存在');
    const result = await this.roomExpRepo
      .createQueryBuilder()
      .update()
      .set({ status: 'archived' })
      .where('id IN (:...ids)', { ids: uniqueIds })
      .execute();
    return { message: `已批量归档 ${result.affected || 0} 条`, archived: result.affected || 0 };
  }

  async batchRestoreRoomExpenses(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 existing = await this.roomExpRepo.find({ where: { id: In(uniqueIds) } });
    if (existing.length !== uniqueIds.length) throw new NotFoundException('部分费用记录不存在');
    const targetIds = existing.filter((expense) => expense.status === 'archived').map((expense) => expense.id);
    const skipped = existing.length - targetIds.length;
    let restored = 0;
    if (targetIds.length > 0) {
      const billed = await this.dataSource
        .getRepository('bill_items')
        .count({ where: { roomExpenseId: In(targetIds) } });
      if (billed) throw new BadRequestException('选中记录包含已计入账单的宿舍费用');
      const result = await this.roomExpRepo
        .createQueryBuilder()
        .update()
        .set({ status: 'active' })
        .where('id IN (:...ids)', { ids: targetIds })
        .execute();
      restored = result.affected || 0;
    }
    return { message: `已批量恢复 ${restored} 条宿舍费用`, restored, skipped };
  }

  async purgeRoomExpense(id: number) {
    const e = await this.roomExpRepo.findOne({ where: { id } });
    if (!e) throw new NotFoundException('费用记录不存在');
    if (e.status !== 'archived') throw new BadRequestException('仅已归档费用可以永久删除，请先归档');
    const billed = await this.dataSource
      .getRepository('bill_items')
      .count({ where: { roomExpenseId: id } });
    if (billed) throw new BadRequestException('已计入账单的宿舍费用不能永久删除，请先取消账单');
    await this.roomExpRepo.delete(id);
    return { message: '已永久删除宿舍费用（不可恢复）' };
  }

  async batchPurgeRoomExpenses(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 existing = await this.roomExpRepo.find({ where: { id: In(uniqueIds) } });
    if (existing.length !== uniqueIds.length) throw new NotFoundException('部分费用记录不存在');
    const billed = await this.dataSource
      .getRepository('bill_items')
      .count({ where: { roomExpenseId: In(uniqueIds) } });
    if (billed) throw new BadRequestException('选中记录包含已计入账单的宿舍费用');

    const deleted: number[] = [];
    const skipped: string[] = [];
    for (const e of existing) {
      if (e.status !== 'archived') {
        skipped.push(`记录${e.id}（未归档）`);
        continue;
      }
      await this.roomExpRepo.delete(e.id);
      deleted.push(e.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 updateRoomExpense(id: number, dto: Partial<CreateRoomExpenseDto>) {
    const e = await this.roomExpRepo.findOne({ where: { id } });
    const billed = await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: id } });
    if (billed) throw new BadRequestException('已计入账单的宿舍费用不能修改，请先取消账单');
    if (!e) throw new NotFoundException('费用记录不存在');
    const periodStart = dto.periodStart ?? e.periodStart;
    const periodEnd = dto.periodEnd ?? e.periodEnd;
    this.assertValidPeriod(periodStart, periodEnd);
    if (dto.amount !== undefined) this.assertPositiveAmount(dto.amount);
    if (dto.roomId !== undefined && dto.roomId !== e.roomId) {
      const room = await this.roomRepo.findOne({ where: { id: dto.roomId } });
      if (!room) throw new NotFoundException('宿舍不存在');
    }
    Object.assign(e, dto);
    return this.roomExpRepo.save(e);
  }

  private assertPositiveAmount(amount: number) {
    if (!Number.isFinite(amount) || Math.abs(amount * 100 - Math.round(amount * 100)) > 1e-8) {
      throw new BadRequestException('费用金额最多保留两位小数');
    }
    if (amount <= 0) throw new BadRequestException('费用金额必须大于0');
  }

  private assertValidPeriod(periodStart: string, periodEnd: string) {
    if (!this.isValidDate(periodStart) || !this.isValidDate(periodEnd) || periodEnd < periodStart) {
      throw new BadRequestException('账期无效，结束日期不能早于开始日期');
    }
  }

  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;
  }

  async createStudentUtilityBill(dto: CreateStudentUtilityBillDto, userId?: number) {
    this.assertValidPeriod(dto.periodStart, dto.periodEnd);
    this.assertPositiveAmount(dto.amount);
    const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
    if (!student) throw new NotFoundException('学生不存在');
    const expense = {
      studentId: dto.studentId,
      expenseType: dto.expenseType,
      amount: dto.amount,
      expenseDate: dto.periodEnd,
      description: dto.description || (dto.expenseType === 'water' ? '学生水费' : '学生电费'),
      recordedBy: userId,
      billId: null,
    } as PersonalExpense;
    return this.billsService.createImmediatePersonalBill(expense, dto.periodStart, dto.periodEnd, userId);
  }

  // 个人附加费
  async createPersonalExpense(dto: CreatePersonalExpenseDto, userId?: number) {
    return this.operations.createPersonalExpense(dto, userId);
  }

  async findPersonalExpenses(query?: { studentId?: number; status?: 'active' | 'archived' }) {
    return this.operations.findPersonalExpenses(query);
  }

  async deletePersonalExpense(id: number) {
    return this.operations.deletePersonalExpense(id);
  }

  async batchDeletePersonalExpenses(ids: number[]) {
    return this.operations.batchDeletePersonalExpenses(ids);
  }

  async batchRestorePersonalExpenses(ids: number[]) {
    return this.operations.batchRestorePersonalExpenses(ids);
  }

  async purgePersonalExpense(id: number) {
    return this.operations.purgePersonalExpense(id);
  }

  async batchPurgePersonalExpenses(ids: number[]) {
    return this.operations.batchPurgePersonalExpenses(ids);
  }

  async updatePersonalExpense(id: number, dto: Partial<CreatePersonalExpenseDto>) {
    return this.operations.updatePersonalExpense(id, dto);
  }

  async batchImportUtilityExpenses(
    rows: {
      periodStr: string;
      roomNumber: string;
      electricityAmount: number;
      electricityFee: number;
      waterAmount: number;
      waterFee: number;
      totalFee: number;
    }[],
    userId?: number,
  ) {
    return this.operations.batchImportUtilityExpenses(rows, userId);
  }

  async batchImportPersonalExpenses(
    rows: {
      studentName: string;
      expenseType: string;
      amount: number;
      expenseDate: string;
      description?: string;
    }[],
    userId?: number,
  ) {
    return this.operations.batchImportPersonalExpenses(rows, userId);
  }
}
