import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository, In, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
import {
  AttendanceRecord,
  Class,
  ClassSchedule,
  ClassStudent,
  AttendanceSession,
  AttendancePeriodConfig,
  ScheduleType,
} from '../entities';
import { toMinutes, isClassStudentActiveOnDate } from './attendance-time';
import type { BatchCreateAttendanceDto, GenerateAttendanceFromSchedulesDto, GenerateFromSchedulesDto, SaveAttendancePeriodConfigsDto } from './dto/attendance.dto';

@Injectable()
export class AttendanceGenerationService {
    private readonly defaultAttendancePeriods = [
    { periodKey: 'morning_reading', label: '早自习', startTime: '07:30', endTime: '08:30', sortOrder: 1 },
    { periodKey: 'morning', label: '早课', startTime: '09:00', endTime: '12:00', sortOrder: 2 },
    { periodKey: 'afternoon', label: '晚课', startTime: '14:00', endTime: '17:00', sortOrder: 3 },
    { periodKey: 'evening_study', label: '晚自习', startTime: '18:30', endTime: '21:00', sortOrder: 4 },
  ] as const;

  constructor(
    @InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
    @InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
    @InjectRepository(Class) private classRepo: Repository<Class>,
    @InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
    @InjectRepository(AttendanceSession) private attendanceSessionRepo: Repository<AttendanceSession>,
    @InjectRepository(AttendancePeriodConfig) private attendancePeriodConfigRepo: Repository<AttendancePeriodConfig>,
    private dataSource: DataSource,
  ) {}

  async batchCreate(dto: BatchCreateAttendanceDto) {
    if (!dto.records || dto.records.length === 0) {
      throw new BadRequestException('records array must not be empty');
    }

    const entities = dto.records.map((r) => {
      const entity = this.attendanceRepo.create({
        studentId: r.studentId,
        classId: r.classId ?? undefined,
        attendanceDate: r.attendanceDate,
        session: r.session,
        status: r.status,
        remark: r.remark,
        source: r.source || 'manual',
      });
      return entity;
    });

    const saved = await this.attendanceRepo.save(entities);
    return { count: saved.length, records: saved };
  }

  // ── Generate attendance records from class schedules ──
  async generateAttendanceFromSchedules(dto: GenerateAttendanceFromSchedulesDto) {
    const { classId, dateFrom, dateTo } = dto;

    if (dateFrom > dateTo) {
      throw new BadRequestException('dateFrom must not be later than dateTo');
    }

    const cls = await this.classRepo.findOne({ where: { id: classId } });
    if (!cls) {
      throw new NotFoundException(`Class ${classId} not found`);
    }

    const schedules = await this.scheduleRepo.find({
      where: {
        classId,
        scheduleType: ScheduleType.INTERNAL,
        status: 'active',
        startDate: LessThanOrEqual(dateTo),
        endDate: MoreThanOrEqual(dateFrom),
      },
    });

    const classStudents = await this.classStudentRepo.find({
      where: { classId, status: In(['active', 'left']) },
      relations: ['student'],
    });

    if (schedules.length === 0 || classStudents.length === 0) {
      return { count: 0, records: [] };
    }

    const existingRecords = await this.attendanceRepo.find({
      where: { classId, attendanceDate: Between(dateFrom, dateTo) },
    });
    const existingKeys = new Set(
      existingRecords.map((r) => `${r.studentId}|${r.attendanceDate}|${r.session}`),
    );

    const entities: AttendanceRecord[] = [];
    const end = new Date(dateTo);
    for (let d = new Date(dateFrom); d <= end; d.setDate(d.getDate() + 1)) {
      const dateStr = d.toISOString().slice(0, 10);
      const weekDay = d.getDay() === 0 ? 7 : d.getDay();

      for (const sched of schedules) {
        if (sched.weekDay !== weekDay) continue;
        if (dateStr < sched.startDate || dateStr > sched.endDate) continue;

        const session = await this.mapScheduleTimeToSession(sched.startTime);
        const classStudentsForDate = classStudents.filter((cs) =>
          isClassStudentActiveOnDate(cs, dateStr),
        );
        for (const cs of classStudentsForDate) {
          const key = `${cs.studentId}|${dateStr}|${session}`;
          if (existingKeys.has(key)) continue;

          const entity = this.attendanceRepo.create({
            studentId: cs.studentId,
            classId,
            attendanceDate: dateStr,
            session,
            status: 'pending',
            source: 'schedule',
          });
          entities.push(entity);
          existingKeys.add(key);
        }
      }
    }

    const saved = await this.attendanceRepo.save(entities);
    return { count: saved.length, records: saved };
  }

  // ── Generate attendance records from schedules (optional date range, defaults to current week) ──
  async generateFromSchedules(
    dto: GenerateFromSchedulesDto,
  ): Promise<{ count: number; records: AttendanceRecord[] }> {
    const { classId, startDate, endDate } = dto;

    // Default to current week (Monday–Sunday)
    const now = new Date();
    const dayOfWeek = now.getDay();
    const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
    const monday = new Date(now);
    monday.setDate(now.getDate() + mondayOffset);
    monday.setHours(0, 0, 0, 0);
    const sunday = new Date(monday);
    sunday.setDate(monday.getDate() + 6);
    sunday.setHours(23, 59, 59, 999);

    const dateFrom = startDate ?? monday.toISOString().slice(0, 10);
    const dateTo = endDate ?? sunday.toISOString().slice(0, 10);

    return this.generateAttendanceFromSchedules({
      classId,
      dateFrom,
      dateTo,
    });
  }

  private toMinutes(time: string): number {
    const [hour, minute] = time.split(':').map(Number);
    return hour * 60 + minute;
  }

  private getCourseClock(date: Date): { date: string; minutes: number } {
    const parts = Object.fromEntries(
      new Intl.DateTimeFormat('en-CA', {
        timeZone: 'Asia/Shanghai',
        year: 'numeric', month: '2-digit', day: '2-digit',
        hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
      }).formatToParts(date).filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]),
    );
    return {
      date: `${parts.year}-${parts.month}-${parts.day}`,
      minutes: Number(parts.hour) * 60 + Number(parts.minute),
    };
  }

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

  private async ensureAttendancePeriodConfigs() {
    const count = await this.attendancePeriodConfigRepo.count();
    if (count === 0) {
      await this.attendancePeriodConfigRepo.save(
        this.defaultAttendancePeriods.map((period) => this.attendancePeriodConfigRepo.create({
          ...period,
          enabled: true,
        })),
      );
    }
    return this.attendancePeriodConfigRepo.find({ order: { sortOrder: 'ASC', id: 'ASC' } });
  }

  async getAttendancePeriodConfigs() {
    return this.ensureAttendancePeriodConfigs();
  }

  async getRefreshableSchedules(date: string, classId?: number, session?: string, accessibleClassIds?: number[]) {
    const parsedDate = new Date(`${date}T00:00:00`);
    if (Number.isNaN(parsedDate.getTime())) throw new BadRequestException('无效日期');
    const weekDay = parsedDate.getDay() === 0 ? 7 : parsedDate.getDay();
    const qb = this.scheduleRepo
      .createQueryBuilder('schedule')
      .where('schedule.scheduleType = :scheduleType', { scheduleType: ScheduleType.INTERNAL })
      .andWhere('schedule.status = :status', { status: 'active' })
      .andWhere('schedule.classId IS NOT NULL')
      .andWhere('schedule.weekDay = :weekDay', { weekDay })
      .andWhere('schedule.startDate <= :date', { date })
      .andWhere('schedule.endDate >= :date', { date });

    if (classId) {
      qb.andWhere('schedule.classId = :classId', { classId });
    } else if (accessibleClassIds) {
      if (accessibleClassIds.length === 0) return [];
      qb.andWhere('schedule.classId IN (:...accessibleClassIds)', { accessibleClassIds });
    }

    const schedules = await qb.orderBy('schedule.startTime', 'ASC').getMany();
    if (!session) return schedules;

    const matchedSchedules: ClassSchedule[] = [];
    for (const schedule of schedules) {
      if ((await this.mapScheduleTimeToSession(schedule.startTime)) === session) {
        matchedSchedules.push(schedule);
      }
    }
    return matchedSchedules;
  }

  async saveAttendancePeriodConfigs(dto: SaveAttendancePeriodConfigsDto) {
    const seen = new Set<string>();
    const normalized = dto.periods.map((period, index) => {
      const periodKey = period.periodKey.trim();
      const label = period.label.trim();
      if (!periodKey || !label) throw new BadRequestException('时段标识和名称不能为空');
      if (seen.has(periodKey)) throw new BadRequestException(`时段标识 ${periodKey} 重复`);
      seen.add(periodKey);
      if (toMinutes(period.endTime) <= toMinutes(period.startTime)) {
        throw new BadRequestException(`${label} 的结束时间必须晚于开始时间`);
      }
      return {
        periodKey,
        label,
        startTime: period.startTime,
        endTime: period.endTime,
        sortOrder: period.sortOrder ?? index + 1,
        enabled: period.enabled ?? true,
      };
    }).sort((left, right) => left.sortOrder - right.sortOrder);

    // 按开始时间排序后再检查重叠，避免 sortOrder 与时间顺序不一致时漏检
    const sortedByTime = [...normalized].sort(
      (left, right) => toMinutes(left.startTime) - toMinutes(right.startTime),
    );
    for (let index = 1; index < sortedByTime.length; index += 1) {
      const previous = sortedByTime[index - 1];
      const current = sortedByTime[index];
      if (previous.enabled && current.enabled && toMinutes(current.startTime) < toMinutes(previous.endTime)) {
        throw new BadRequestException(`${previous.label} 和 ${current.label} 时间段不能重叠`);
      }
    }

    await this.attendancePeriodConfigRepo.clear();
    await this.attendancePeriodConfigRepo.save(
      normalized.map((period) => this.attendancePeriodConfigRepo.create(period)),
    );
    return this.getAttendancePeriodConfigs();
  }

  async resetAttendancePeriodConfigs() {
    await this.attendancePeriodConfigRepo.clear();
    return this.ensureAttendancePeriodConfigs();
  }

  private mapLessonScheduleTimeToSession(startTime: string): string {
    const hour = parseInt(startTime.slice(0, 2), 10);
    if (hour < 8) return 'morning_reading';
    if (hour < 12) return 'morning';
    if (hour < 17) return 'afternoon';
    if (hour < 20) return 'evening_study';
    return 'night_check';
  }

  private async mapScheduleTimeToSession(startTime: string): Promise<string> {
    const startMinutes = toMinutes(startTime);
    const periods = (await this.ensureAttendancePeriodConfigs()).filter((period) => period.enabled);
    const matched = periods.find((period) => {
      const periodStart = toMinutes(period.startTime);
      const periodEnd = toMinutes(period.endTime);
      return startMinutes >= periodStart && startMinutes < periodEnd;
    });
    if (matched) return matched.periodKey;
    throw new BadRequestException(`课程开始时间 ${startTime} 未匹配到考勤时段，请先配置考勤时段`);
  }

}
