import { BadRequestException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { Bed } from '../entities/bed.entity';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { RoomInspection } from '../entities/room-inspection.entity';
import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { occupancyWhereOnDate } from './room-occupancy-date';

interface InspectorIdentity {
  id?: number;
  username?: string;
}

@Injectable()
export class RoomInspectionsService implements OnApplicationBootstrap {
  private readonly logger = new Logger(RoomInspectionsService.name);
  private settling = false;

  constructor(
    @InjectRepository(RoomInspection)
    private readonly inspectionRepo: Repository<RoomInspection>,
    @InjectRepository(RoomInspectionDetail)
    private readonly detailRepo: Repository<RoomInspectionDetail>,
    @InjectRepository(Room)
    private readonly roomRepo: Repository<Room>,
    @InjectRepository(Occupancy)
    private readonly occupancyRepo: Repository<Occupancy>,
    private readonly dataSource: DataSource,
    private readonly operationLogs: OperationLogsService,
  ) {}

  async onApplicationBootstrap(): Promise<void> {
    await this.settlePreviousDay().catch((error) => {
      this.logger.error('补记昨日宿舍查寝失败', error instanceof Error ? error.stack : String(error));
    });
  }

  @Cron('5 0 * * *', { timeZone: 'Asia/Shanghai' })
  async settlePreviousDay(now = new Date()): Promise<void> {
    if (this.settling) return;
    this.settling = true;
    try {
      const today = this.getChinaDate(now);
      const targetDate = this.shiftDate(today, -1);
      await this.settleDate(targetDate);
    } finally {
      this.settling = false;
    }
  }

  async submit(
    roomId: number,
    inspectionDate: string,
    presentOccupancyIds: number[],
    inspector: InspectorIdentity,
  ) {
    this.assertToday(inspectionDate);
    const uniquePresentIds = [...new Set(presentOccupancyIds)];

    return this.dataSource.transaction(async (manager) => {
      const room = await this.lockRoom(manager, roomId, false);
      const occupancies = await this.findOccupanciesForDate(manager, roomId, inspectionDate);
      const allowedIds = new Set(occupancies.map((occupancy) => occupancy.id));
      const invalidIds = uniquePresentIds.filter((id) => !allowedIds.has(id));
      if (invalidIds.length > 0) {
        throw new BadRequestException(`存在不属于该宿舍当日住户的入住记录: ${invalidIds.join(', ')}`);
      }

      const inspectionRepo = manager.getRepository(RoomInspection);
      const detailRepo = manager.getRepository(RoomInspectionDetail);
      let inspection = await inspectionRepo.findOne({
        where: { roomId, inspectionDate },
      });
      const isUpdate = !!inspection;
      if (!inspection) {
        inspection = inspectionRepo.create({ roomId, inspectionDate });
      }
      inspection.inspectorId = inspector.id ?? null;
      inspection.inspectorName = inspector.username || '未知用户';
      inspection.source = 'manual';
      inspection.submittedAt = new Date();
      inspection = await inspectionRepo.save(inspection);

      await detailRepo.delete({ inspectionId: inspection.id });
      const presentSet = new Set(uniquePresentIds);
      const details = occupancies.map((occupancy) =>
        detailRepo.create({
          inspectionId: inspection.id,
          occupancyId: occupancy.id,
          studentId: occupancy.studentId,
          bedId: occupancy.bedId ?? null,
          status: presentSet.has(occupancy.id) ? 'present' : 'absent',
          studentNameSnapshot: occupancy.student?.name || '未知学生',
          bedNumberSnapshot: occupancy.bed?.bedNumber || null,
        }),
      );
      if (details.length > 0) await detailRepo.save(details);

      return {
        inspection: { ...inspection, details },
        roomNumber: room.roomNumber,
        isUpdate,
        presentNames: details
          .filter((detail) => detail.status === 'present')
          .map((detail) => detail.studentNameSnapshot),
        absentNames: details
          .filter((detail) => detail.status === 'absent')
          .map((detail) => detail.studentNameSnapshot),
      };
    });
  }

  async getByRoomsAndDate(roomIds: number[], inspectionDate: string) {
    if (roomIds.length === 0) return new Map<number, RoomInspection>();
    const inspections = await this.inspectionRepo
      .createQueryBuilder('inspection')
      .leftJoinAndSelect('inspection.details', 'detail')
      .where('inspection.roomId IN (:...roomIds)', { roomIds })
      .andWhere('inspection.inspectionDate = :inspectionDate', { inspectionDate })
      .getMany();
    return new Map(inspections.map((inspection) => [inspection.roomId, inspection]));
  }

  async settleDate(inspectionDate: string): Promise<number> {
    const existing = await this.inspectionRepo.find({ where: { inspectionDate } });
    const existingRoomIds = new Set(existing.map((inspection) => inspection.roomId));
    const occupancies = await this.findAllOccupanciesForDate(this.dataSource.manager, inspectionDate);
    const byRoom = new Map<number, Occupancy[]>();
    for (const occupancy of occupancies) {
      if (existingRoomIds.has(occupancy.roomId)) continue;
      const roomOccupancies = byRoom.get(occupancy.roomId) ?? [];
      roomOccupancies.push(occupancy);
      byRoom.set(occupancy.roomId, roomOccupancies);
    }
    if (byRoom.size === 0) return 0;

    const lastManualInspection = await this.inspectionRepo.findOne({
      where: { inspectionDate, source: 'manual' },
      order: { submittedAt: 'DESC' },
    });
    const inspectorId = lastManualInspection?.inspectorId ?? null;
    const inspectorName = lastManualInspection?.inspectorName || '系统自动判定';
    let created = 0;

    for (const [roomId, roomOccupancies] of byRoom) {
      const result = await this.dataSource.transaction(async (manager) => {
        await this.lockRoom(manager, roomId, true);
        const inspectionRepo = manager.getRepository(RoomInspection);
        const detailRepo = manager.getRepository(RoomInspectionDetail);
        const duplicate = await inspectionRepo.findOne({ where: { roomId, inspectionDate } });
        if (duplicate) return null;
        const inspection = await inspectionRepo.save(
          inspectionRepo.create({
            inspectionDate,
            roomId,
            inspectorId,
            inspectorName,
            source: 'automatic',
            submittedAt: new Date(),
          }),
        );
        const details = roomOccupancies.map((occupancy) =>
          detailRepo.create({
            inspectionId: inspection.id,
            occupancyId: occupancy.id,
            studentId: occupancy.studentId,
            bedId: occupancy.bedId ?? null,
            status: 'absent',
            studentNameSnapshot: occupancy.student?.name || '未知学生',
            bedNumberSnapshot: occupancy.bed?.bedNumber || null,
          }),
        );
        await detailRepo.save(details);
        return { inspection, details };
      });
      if (!result) continue;
      created++;
      const room = await this.roomRepo.findOne({ where: { id: roomId } });
      await this.operationLogs.log({
        userId: inspectorId ?? undefined,
        username: inspectorName,
        module: '宿舍查寝',
        action: '自动补记缺勤',
        targetId: roomId,
        targetType: 'room',
        detail: `查寝日期: ${inspectionDate}, 宿舍: ${room?.roomNumber || roomId}, 缺勤: ${result.details.map((detail) => detail.studentNameSnapshot).join('、') || '无'}`,
      });
    }
    return created;
  }

  private async lockRoom(
    manager: EntityManager,
    roomId: number,
    allowArchived: boolean,
  ): Promise<Room> {
    let query = manager.createQueryBuilder(Room, 'room').where('room.id = :roomId', { roomId });
    if (['mysql', 'mariadb', 'postgres', 'cockroachdb'].includes(this.dataSource.options.type)) {
      query = query.setLock('pessimistic_write');
    }
    const room = await query.getOne();
    if (!room) throw new BadRequestException('宿舍不存在');
    if (!allowArchived && room.status === 'archived') {
      throw new BadRequestException('已归档宿舍不能查寝');
    }
    return room;
  }

  private findOccupanciesForDate(manager: EntityManager, roomId: number, date: string) {
    return manager.getRepository(Occupancy).find({
      where: occupancyWhereOnDate(date, roomId),
      relations: ['student', 'bed'],
      order: { id: 'ASC' },
    });
  }

  private findAllOccupanciesForDate(manager: EntityManager, date: string) {
    return manager.getRepository(Occupancy).find({
      where: occupancyWhereOnDate(date),
      relations: ['student', 'bed'],
      order: { roomId: 'ASC', id: 'ASC' },
    });
  }

  private assertToday(date: string): void {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new BadRequestException('查寝日期格式错误');
    const today = this.getChinaDate(new Date());
    if (date < today) throw new BadRequestException('历史日期的查寝记录不可更改');
    if (date > today) throw new BadRequestException('不能提前提交未来日期的查寝记录');
  }

  private getChinaDate(now: Date): string {
    return new Intl.DateTimeFormat('en-CA', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    }).format(now);
  }

  private shiftDate(date: string, days: number): string {
    const shifted = new Date(`${date}T12:00:00Z`);
    shifted.setUTCDate(shifted.getUTCDate() + days);
    return shifted.toISOString().slice(0, 10);
  }
}
