import {
  Injectable,
  NotFoundException,
  ConflictException,
  ForbiddenException,
  BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Not, Repository } from 'typeorm';
import {
  ClassSchedule,
  Class,
  Classroom,
  ClassroomStatus,
  ClassroomRental,
  ClassTeacher,
  AttendanceSession,
} from '../entities';
import {
  CreateScheduleDto,
  UpdateScheduleDto,
  QueryScheduleDto,
  WeeklyViewQueryDto,
} from './dto/schedule.dto';

const SCHEDULE_GAP_MINUTES = 10;
const ACTIVE_SCHEDULE_STATUS = 'active';
const INACTIVE_SCHEDULE_STATUSES = ['inactive', 'cancelled'] as const;
type ScheduleStatus = typeof ACTIVE_SCHEDULE_STATUS | (typeof INACTIVE_SCHEDULE_STATUSES)[number];
const SCHEDULE_STATUSES: readonly ScheduleStatus[] = [ACTIVE_SCHEDULE_STATUS, ...INACTIVE_SCHEDULE_STATUSES];

function shiftTime(time: string, minutes: number): string {
  const [hours, minutePart] = time.split(':').map(Number);
  const shifted = Math.min(24 * 60, Math.max(0, hours * 60 + minutePart + minutes));
  const shiftedHours = Math.floor(shifted / 60);
  const shiftedMinutes = shifted % 60;
  return `${String(shiftedHours).padStart(2, '0')}:${String(shiftedMinutes).padStart(2, '0')}`;
}

@Injectable()
export class SchedulesService {
  constructor(
    @InjectRepository(ClassSchedule)
    private readonly scheduleRepo: Repository<ClassSchedule>,
    @InjectRepository(Class) private readonly classRepo: Repository<Class>,
    @InjectRepository(Classroom) private readonly classroomRepo: Repository<Classroom>,
    @InjectRepository(ClassroomRental)
    private readonly rentalRepo: Repository<ClassroomRental>,
    @InjectRepository(ClassTeacher)
    private readonly classTeacherRepo: Repository<ClassTeacher>,
    @InjectRepository(AttendanceSession)
    private readonly attendanceSessionRepo: Repository<AttendanceSession>,
  ) {}

  async getAccessibleClassIds(userId: number, canManageAll = false): Promise<number[] | undefined> {
    if (canManageAll) return undefined;
    const assignments = await this.classTeacherRepo.find({ where: { userId } });
    return [...new Set(assignments.map((assignment) => assignment.classId))];
  }

  async assertClassAccess(userId: number, classId: number, canManageAll = false): Promise<void> {
    if (canManageAll) return;
    const assignment = await this.classTeacherRepo.findOne({ where: { userId, classId } });
    if (!assignment) throw new ForbiddenException('只能管理自己被分配班级的排课');
  }

  maskScheduleOccupancy(schedule: ClassSchedule) {
    return {
      id: null,
      classId: null,
      classroomId: schedule.classroomId,
      weekDay: schedule.weekDay,
      startTime: schedule.startTime,
      endTime: schedule.endTime,
      attendanceAdvanceMinutes: schedule.attendanceAdvanceMinutes,
      startDate: schedule.startDate,
      endDate: schedule.endDate,
      subject: '已占用',
      teacherId: null,
      scheduleType: schedule.scheduleType,
      status: schedule.status,
      notes: null,
      canViewDetails: false,
    };
  }

  async getLookups(accessibleClassIds?: number[]) {
    const classes = accessibleClassIds
      ? accessibleClassIds.length > 0
        ? await this.classRepo.find({
            where: { id: In(accessibleClassIds) },
            select: ['id', 'name', 'code'],
            order: { name: 'ASC' },
          })
        : []
      : await this.classRepo.find({
          select: ['id', 'name', 'code'],
          order: { name: 'ASC' },
        });

    const classrooms = await this.classroomRepo.find({
      where: { status: ClassroomStatus.AVAILABLE },
      select: ['id', 'name', 'building', 'floor', 'roomType'],
      order: { building: 'ASC', name: 'ASC' },
    });

    return {
      classes,
      classrooms,
    };
  }

  async findAll(query: QueryScheduleDto, accessibleClassIds?: number[]) {
    const qb = this.scheduleRepo.createQueryBuilder('cs');

    if (query.classroomId)
      qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId });
    if (query.classId) qb.andWhere('cs.classId = :classId', { classId: query.classId });
    else if (accessibleClassIds) {
      if (accessibleClassIds.length === 0) return [];
      qb.andWhere('cs.classId IN (:...accessibleClassIds)', { accessibleClassIds });
    }
    if (query.weekDay) qb.andWhere('cs.weekDay = :weekDay', { weekDay: query.weekDay });
    if (query.startDate) qb.andWhere('cs.startDate >= :startDate', { startDate: query.startDate });
    if (query.endDate) qb.andWhere('cs.endDate <= :endDate', { endDate: query.endDate });

    qb.orderBy('cs.weekDay', 'ASC').addOrderBy('cs.startTime', 'ASC');

    return qb.getMany();
  }

  async getClassTeachers(classId: number) {
    const teachers = await this.classTeacherRepo.find({
      where: { classId },
      relations: ['user'],
      order: { roleType: 'ASC', subject: 'ASC' },
    });
    return teachers.map((teacher) => ({
      id: teacher.id,
      userId: teacher.userId,
      username: teacher.user?.username,
      name: teacher.user?.name,
      roleType: teacher.roleType,
      subject: teacher.subject,
    }));
  }

  private async normalizeTeacherForSchedule<
    T extends { classId?: number; subject?: string; teacherId?: number | null },
  >(dto: T): Promise<T> {
    if (!dto.classId || !dto.subject || dto.teacherId) return dto;
    const teachers = await this.classTeacherRepo.find({
      where: { classId: dto.classId, roleType: 'subject_teacher', subject: dto.subject },
    });
    if (teachers.length === 1) {
      dto.teacherId = teachers[0].userId;
    }
    return dto;
  }

  private async assertTeacherAssignedToClass(
    classId: number | null | undefined,
    teacherId: number | null | undefined,
  ) {
    if (!classId || !teacherId) return;
    const assignment = await this.classTeacherRepo.findOne({
      where: { classId, userId: teacherId },
    });
    if (!assignment) throw new BadRequestException('只能选择该班级已配置的教师');
  }

  async findOne(id: number) {
    const schedule = await this.scheduleRepo.findOne({ where: { id } });
    if (!schedule) throw new NotFoundException('排课记录不存在');
    return schedule;
  }

  private assertValidScheduleStatus(status: string): asserts status is ScheduleStatus {
    if (!SCHEDULE_STATUSES.includes(status as ScheduleStatus)) {
      throw new BadRequestException('排课状态无效');
    }
  }

  private async assertClassroomAvailable(classroomId: number) {
    const classroom = await this.classroomRepo.findOne({ where: { id: classroomId } });
    if (!classroom) throw new NotFoundException('教室不存在');
    if (classroom.status !== ClassroomStatus.AVAILABLE) {
      throw new BadRequestException('仅可用教室可以排课');
    }
  }

  private assertValidScheduleRange(
    startTime: string,
    endTime: string,
    startDate: string,
    endDate: string,
  ) {
    if (startTime === endTime) {
      throw new BadRequestException('上课时间和下课时间不能相同');
    }
    if (startTime > endTime) {
      throw new BadRequestException('上课时间不能晚于下课时间');
    }
    if (startDate > endDate) {
      throw new BadRequestException('排课结束日期不能早于开始日期');
    }
  }

  async create(dto: CreateScheduleDto) {
    this.assertValidScheduleRange(dto.startTime, dto.endTime, dto.startDate, dto.endDate);
    await this.assertClassroomAvailable(dto.classroomId);
    await this.normalizeTeacherForSchedule(dto);
    await this.assertTeacherAssignedToClass(dto.classId, dto.teacherId);
    await this.checkConflict(
      dto.classroomId,
      dto.weekDay,
      dto.startTime,
      dto.endTime,
      dto.startDate,
      dto.endDate,
    );

    const schedule = this.scheduleRepo.create(dto);
    const saved = await this.scheduleRepo.save(schedule);
    return this.findOne(saved.id);
  }

  async update(id: number, dto: UpdateScheduleDto) {
    const existing = await this.scheduleRepo.findOne({ where: { id } });
    if (!existing) throw new NotFoundException('排课记录不存在');

    const nextStatus = dto.status ?? existing.status;
    this.assertValidScheduleStatus(nextStatus);

    // If classroom, weekDay, or times are changing, check conflicts excluding self.
    // Inactive/cancelled schedules preserve history but no longer occupy classrooms.
    const classroomId = dto.classroomId ?? existing.classroomId;
    const weekDay = dto.weekDay ?? existing.weekDay;
    const startTime = dto.startTime ?? existing.startTime;
    const endTime = dto.endTime ?? existing.endTime;
    const startDate = dto.startDate ?? existing.startDate;
    const endDate = dto.endDate ?? existing.endDate;
    this.assertValidScheduleRange(startTime, endTime, startDate, endDate);

    if (nextStatus === ACTIVE_SCHEDULE_STATUS) {
      if (dto.classroomId !== undefined && dto.classroomId !== existing.classroomId) {
        await this.assertClassroomAvailable(dto.classroomId);
      } else if (existing.status !== ACTIVE_SCHEDULE_STATUS) {
        await this.assertClassroomAvailable(classroomId);
      }
    }

    if (nextStatus === ACTIVE_SCHEDULE_STATUS) {
      const normalized = await this.normalizeTeacherForSchedule({
        ...dto,
        classId: dto.classId ?? existing.classId ?? undefined,
        subject: dto.subject ?? existing.subject,
      });
      if (dto.teacherId === undefined && normalized.teacherId !== undefined) {
        dto.teacherId = normalized.teacherId;
      }
      const teacherId = dto.teacherId ?? existing.teacherId;
      await this.assertTeacherAssignedToClass(dto.classId ?? existing.classId, teacherId);
      await this.checkConflict(classroomId, weekDay, startTime, endTime, startDate, endDate, id);
    }

    await this.scheduleRepo.update(id, dto);
    return this.findOne(id);
  }

  async remove(id: number) {
    const schedule = await this.scheduleRepo.findOne({ where: { id } });
    if (!schedule) throw new NotFoundException('排课记录不存在');
    if (schedule.status !== ACTIVE_SCHEDULE_STATUS) {
      throw new BadRequestException('排课已停用');
    }

    await this.scheduleRepo.update(id, { status: 'inactive' });
    return { success: true, message: '排课已停用（历史考勤记录已保留）' };
  }

  async checkConflict(
    classroomId: number,
    weekDay: number,
    startTime: string,
    endTime: string,
    startDate: string,
    endDate: string,
    excludeId?: number,
  ) {
    // 为教室换场、整理和人员进出预留时间；恰好间隔 10 分钟允许排课。
    const bufferedStartTime = shiftTime(startTime, -SCHEDULE_GAP_MINUTES);
    const bufferedEndTime = shiftTime(endTime, SCHEDULE_GAP_MINUTES);
    const qb = this.scheduleRepo
      .createQueryBuilder('cs')
      .where('cs.classroomId = :classroomId', { classroomId })
      .andWhere('cs.weekDay = :weekDay', { weekDay })
      .andWhere('cs.status = :status', { status: ACTIVE_SCHEDULE_STATUS })
      .andWhere('cs.startTime < :bufferedEndTime', { bufferedEndTime })
      .andWhere('cs.endTime > :bufferedStartTime', { bufferedStartTime })
      .andWhere('cs.startDate <= :endDate', { endDate })
      .andWhere('cs.endDate >= :startDate', { startDate });

    if (excludeId) qb.andWhere('cs.id != :excludeId', { excludeId });

    const conflicts = await qb.getMany();
    if (conflicts.length > 0) {
      throw new ConflictException(
        `排课之间必须至少间隔 ${SCHEDULE_GAP_MINUTES} 分钟，与以下排课时间过近: ${conflicts.map((c) => `${c.subject}(${c.startTime}-${c.endTime})`).join(', ')}`,
      );
    }

    // 同时检测同一教室在同一日期段是否存在租赁订单（ status != cancelled ）
    const rentalConflicts = await this.rentalRepo
      .createQueryBuilder('r')
      .where('r.classroomId = :classroomId', { classroomId })
      .andWhere('r.status = :activeRental', { activeRental: 'active' })
      .andWhere('r.startDate <= :endDate', { endDate })
      .andWhere('r.endDate >= :startDate', { startDate })
      .getMany();

    if (rentalConflicts.length > 0) {
      throw new ConflictException(
        `该教室在 ${rentalConflicts.map((r) => `${r.startDate}~${r.endDate}`).join('、')} 已被租赁，无法排课`,
      );
    }

    return conflicts;
  }

  async getWeeklyView(query: WeeklyViewQueryDto, accessibleClassIds?: number[]) {
    const qb = this.scheduleRepo.createQueryBuilder('cs');
    if (query.classroomId) {
      qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId });
    }
    if (query.startDate) {
      qb.andWhere('cs.endDate >= :startDate', { startDate: query.startDate });
    }
    if (query.endDate) {
      qb.andWhere('cs.startDate <= :endDate', { endDate: query.endDate });
    }

    const schedules = await qb
      .andWhere('cs.status = :status', { status: ACTIVE_SCHEDULE_STATUS })
      .orderBy('cs.weekDay', 'ASC')
      .addOrderBy('cs.startTime', 'ASC')
      .getMany();

    const allowedClassIds = accessibleClassIds ? new Set(accessibleClassIds) : null;
    const visibleSchedules = schedules.map((schedule) => {
      const canViewDetails =
        allowedClassIds === null ||
        (schedule.classId !== null && allowedClassIds.has(schedule.classId));
      if (canViewDetails) return { ...schedule, canViewDetails: true };

      // Other classes remain visible only as a room/time occupancy block.
      // Do not expose class, subject, teacher, notes, or internal record IDs.
      return this.maskScheduleOccupancy(schedule);
    });

    // Group by classroomId → weekDay
    const matrix: Record<number, Record<number, typeof visibleSchedules>> = {};
    for (const schedule of visibleSchedules) {
      if (!matrix[schedule.classroomId]) matrix[schedule.classroomId] = {};
      if (!matrix[schedule.classroomId][schedule.weekDay])
        matrix[schedule.classroomId][schedule.weekDay] = [];
      matrix[schedule.classroomId][schedule.weekDay].push(schedule);
    }

    return matrix;
  }

  async getClassroomOccupancy(classroomId: number, date?: string) {
    const qb = this.scheduleRepo
      .createQueryBuilder('cs')
      .where('cs.classroomId = :classroomId', { classroomId })
      .andWhere('cs.status = :status', { status: ACTIVE_SCHEDULE_STATUS })
      .andWhere('cs.scheduleType IN (:...scheduleTypes)', {
        scheduleTypes: ['INTERNAL', 'RENTAL'],
      });

    if (date) {
      qb.andWhere('cs.startDate <= :date', { date }).andWhere('cs.endDate >= :date', { date });
    }

    return qb.orderBy('cs.weekDay', 'ASC').addOrderBy('cs.startTime', 'ASC').getMany();
  }
}
