import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository, In, Between } from 'typeorm';
import {
  AttendanceRecord,
  DingAttendanceRaw,
  DingLeaveRaw,
  Class,
  Student,
  ClassSchedule,
  ClassStudent,
  StudentDingMapping,
  ClassTeacher,
  AttendanceSession,
  AttendanceDevice,
  ScheduleType,
} from '../entities';
import { SessionMutex } from './attendance-mutex';
import { attachAttendanceDeviceMappings } from './attendance-device';
import { getCourseClock, mapLessonScheduleTimeToSession, isClassStudentActiveOnDate } from './attendance-time';
import {
  getLessonAttendanceImportDateRange,
  getLessonAttendanceWindow,
  selectDingTalkRecordsForLesson,
  getLessonPunchMetadata,
} from './attendance-dingtalk';
import { buildLessonRecord, resolveLessonStatus } from './attendance-lesson-status';

@Injectable()
export class AttendanceLessonService {
  private readonly sessionMutex = new SessionMutex();

  constructor(
    @InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
    @InjectRepository(DingAttendanceRaw) private dingRawRepo: Repository<DingAttendanceRaw>,
    @InjectRepository(DingLeaveRaw) private dingLeaveRawRepo: Repository<DingLeaveRaw>,
    @InjectRepository(Class) private classRepo: Repository<Class>,
    @InjectRepository(Student) private studentRepo: Repository<Student>,
    @InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
    @InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
    @InjectRepository(StudentDingMapping) private studentDingMappingRepo: Repository<StudentDingMapping>,
    @InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
    @InjectRepository(AttendanceSession) private attendanceSessionRepo: Repository<AttendanceSession>,
    @InjectRepository(AttendanceDevice) private attendanceDeviceRepo: Repository<AttendanceDevice>,
    private dataSource: DataSource,
  ) {}

  async getClassStudentsForLesson(
    classId: number,
    lessonDate: string,
    relations: string[] = [],
  ): Promise<ClassStudent[]> {
    const classStudents = await this.classStudentRepo.find({
      where: { classId, status: In(['active', 'left']) },
      relations,
    });
    return classStudents.filter((classStudent) =>
      isClassStudentActiveOnDate(classStudent, lessonDate),
    );
  }

  /** List classes the current user may select for DingTalk attendance import. */
  private async getScheduleOccurrence(scheduleId: number, lessonDate: string) {
    const schedule = await this.scheduleRepo.findOne({ where: { id: scheduleId } });
    if (!schedule) throw new NotFoundException('排课记录不存在');
    if ((schedule.scheduleType as ScheduleType) !== ScheduleType.INTERNAL || schedule.status !== 'active') {
      throw new BadRequestException('该排课不能进行课程考勤');
    }
    if (schedule.classId == null) throw new BadRequestException('该排课未关联班级');
    if (lessonDate < schedule.startDate || lessonDate > schedule.endDate) {
      throw new BadRequestException('所选日期不在排课有效期内');
    }
    const date = new Date(`${lessonDate}T00:00:00`);
    const weekDay = date.getDay() === 0 ? 7 : date.getDay();
    if (weekDay !== schedule.weekDay) throw new BadRequestException('所选日期不是该课程的上课日');
    return schedule;
  }

  async getLessonAttendance(scheduleId: number, lessonDate: string) {
    const schedule = await this.getScheduleOccurrence(scheduleId, lessonDate);
    const session = await this.attendanceSessionRepo.findOne({
      where: { scheduleId, lessonDate },
    });
    const records = session
      ? await this.attendanceRepo.find({
          where: { attendanceSessionId: session.id },
          relations: ['student'],
          order: { studentId: 'ASC' },
        })
      : [];
    return { schedule, session, records: await attachAttendanceDeviceMappings(records, this.attendanceDeviceRepo, schedule.classId) };
  }

  getLessonAttendanceImportDateRange(
    schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
    lessonDate: string,
  ): { startDate: string; endDate: string } {
    return getLessonAttendanceImportDateRange(schedule, lessonDate);
  }
  async createLessonAttendanceFromDingTalk(
    scheduleId: number,
    lessonDate: string,
    userId: number,
    finalize = false,
  ) {
    const schedule = await this.getScheduleOccurrence(scheduleId, lessonDate);
    const now = new Date();
    const courseClock = getCourseClock(now);
    const today = courseClock.date;
    if (lessonDate > today) throw new BadRequestException('课程尚未开始，不能拉取考勤');
    if (lessonDate === today) {
      const [hour, minute] = schedule.startTime.split(':').map(Number);
      const startMinute = hour * 60 + minute;
      const currentMinute = courseClock.minutes;
      if (currentMinute < startMinute) {
        throw new BadRequestException('课程尚未开始，不能拉取考勤');
      }
    }

    const existing = await this.attendanceSessionRepo.findOne({
      where: { scheduleId, lessonDate },
    });

    if (existing) {
      if (
        existing.status !== 'in_progress' &&
        existing.status !== 'completed' &&
        !(finalize && existing.status === 'settling')
      ) {
        throw new BadRequestException('课程考勤正在结算');
      }

      // Refresh latest DingTalk data even after automatic settlement; late-arriving punches
      // may legitimately change a DingTalk-generated absence to present.
      return this.dataSource.transaction(async (manager) => {
        const sessionRepo = manager.getRepository(AttendanceSession);
        const recordRepo = manager.getRepository(AttendanceRecord);
        const rawByStudent = await this.fetchDingTalkRawByStudent(schedule.classId!, schedule, lessonDate);
        const effectiveFinalize = finalize || existing.status === 'completed';
        const existingRecords = await recordRepo.find({
          where: { attendanceSessionId: existing.id },
          order: { studentId: 'ASC' },
        });
        const classStudents = await this.getClassStudentsForLesson(
          schedule.classId!,
          lessonDate,
          ['student'],
        );
        const studentsById = new Map(
          classStudents.map((classStudent) => [classStudent.studentId, classStudent.student]),
        );
        const existingStudentIds = new Set(existingRecords.map((record) => record.studentId));

        const lessonSessionKey = mapLessonScheduleTimeToSession(schedule.startTime);
        const updatedRecords = await Promise.all(
          existingRecords.map(async (record) => {
            record.student = studentsById.get(record.studentId)!;
            // Preserve manual corrections only while the lesson is still in progress.
            if (!finalize && record.source !== 'dingtalk') return record;

            const raw = selectDingTalkRecordsForLesson(
              rawByStudent.get(record.studentId) ?? [],
              schedule,
              lessonDate,
            );
            const resolved = await resolveLessonStatus(
              this.dingLeaveRawRepo,
              record.studentId,
              raw,
              schedule,
              lessonDate,
              effectiveFinalize,
            );
            record.status = resolved.status;
            Object.assign(record, getLessonPunchMetadata(
                raw,
                lessonDate,
                schedule.startTime,
              ));
            record.remark = resolved.remark ?? null;
            return record;
          }),
        );
        for (const classStudent of classStudents) {
          if (existingStudentIds.has(classStudent.studentId)) continue;
          updatedRecords.push(
            await buildLessonRecord(
              recordRepo,
              this.dingLeaveRawRepo,
              rawByStudent,
              classStudent,
              schedule,
              lessonDate,
              lessonSessionKey,
              existing.id,
              scheduleId,
              effectiveFinalize,
            ),
          );
        }

        const saved = await recordRepo.save(updatedRecords);
        if (finalize) {
          existing.status = 'completed';
          existing.completedBy = userId;
          existing.completedAt = new Date();
          await sessionRepo.save(existing);
        }
        return { schedule, session: existing, records: await attachAttendanceDeviceMappings(saved, this.attendanceDeviceRepo, schedule.classId) };
      });
    }

    // First pull: create session and records atomically
    return this.dataSource.transaction(async (manager) => {
      const sessionRepo = manager.getRepository(AttendanceSession);
      const recordRepo = manager.getRepository(AttendanceRecord);
      const rawByStudent = await this.fetchDingTalkRawByStudent(schedule.classId!, schedule, lessonDate);

      const classStudents = await this.getClassStudentsForLesson(
        schedule.classId!,
        lessonDate,
        ['student'],
      );
      if (classStudents.length === 0) throw new BadRequestException('该班级暂无在读学生');

      let session: AttendanceSession;
      try {
        session = await sessionRepo.save(
          sessionRepo.create({
            scheduleId,
            classId: schedule.classId!,
            lessonDate,
            status: 'in_progress',
            startedBy: userId,
            startedAt: new Date(),
          }),
        );
      } catch (err: unknown) {
        const code = (err as Record<string, unknown>).code;
        const errno = (err as Record<string, unknown>).errno;
        // MySQL: ER_DUP_ENTRY or errno 1062
        if (code === 'ER_DUP_ENTRY' || errno === 1062) {
          const existing = await sessionRepo.findOne({
            where: { scheduleId, lessonDate },
          });
          if (existing) {
            session = existing;
            const existingRecords = await recordRepo.find({
              where: { attendanceSessionId: session.id },
              relations: ['student'],
              order: { studentId: 'ASC' },
            });
            return { schedule, session, records: await attachAttendanceDeviceMappings(existingRecords, this.attendanceDeviceRepo, schedule.classId) };
          }
        }
        throw err;
      }

      const lessonSessionKey = mapLessonScheduleTimeToSession(schedule.startTime);
      const records = await Promise.all(
        classStudents.map((classStudent) =>
          buildLessonRecord(
            recordRepo,
            this.dingLeaveRawRepo,
            rawByStudent,
            classStudent,
            schedule,
            lessonDate,
            lessonSessionKey,
            session.id,
            scheduleId,
            finalize,
          ),
        ),
      );
      const saved = await recordRepo.save(records);
      if (finalize) {
        session.status = 'completed';
        session.completedBy = userId;
        session.completedAt = new Date();
        session = await sessionRepo.save(session);
      }
      return { schedule, session, records: await attachAttendanceDeviceMappings(saved, this.attendanceDeviceRepo, schedule.classId) };
    });
  }

  private async fetchDingTalkRawByStudent(
    classId: number,
    schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
    lessonDate: string,
  ): Promise<Map<number, DingAttendanceRaw[]>> {
    const classStudents = await this.getClassStudentsForLesson(classId, lessonDate);
    if (classStudents.length === 0) return new Map();
    const studentIds = classStudents.map((cs) => cs.studentId);
    const window = getLessonAttendanceWindow(schedule, lessonDate);
    const rawRecords = await this.dingRawRepo.find({
      where: {
        attendanceDate: Between(window.dateFrom, window.dateTo),
        matchedStudentId: In(studentIds),
      },
    });
    const rawByStudent = new Map<number, DingAttendanceRaw[]>();
    for (const raw of rawRecords) {
      if (raw.matchedStudentId == null) continue;
      const arr = rawByStudent.get(raw.matchedStudentId) ?? [];
      arr.push(raw);
      rawByStudent.set(raw.matchedStudentId, arr);
    }
    return rawByStudent;
  }

  async completeLessonAttendance(sessionId: number, userId: number) {
    return this.sessionMutex.runExclusive(sessionId, () =>
      this.dataSource.transaction(async (manager) => {
        const sessionRepo = manager.getRepository(AttendanceSession);
        const recordRepo = manager.getRepository(AttendanceRecord);

        const session = await sessionRepo.findOne({ where: { id: sessionId } });
        if (!session) throw new NotFoundException('课程考勤场次不存在');

        // Re-check under lock: if already completed, return current state idempotently
        if (session.status === 'completed') {
          const records = await recordRepo.find({
            where: { attendanceSessionId: sessionId },
            relations: ['student'],
            order: { studentId: 'ASC' },
          });
          return { session, records: await attachAttendanceDeviceMappings(records, this.attendanceDeviceRepo, session.classId) };
        }

        const pendingRecords = await recordRepo.count({
          where: { attendanceSessionId: sessionId, status: 'pending' },
        });
        if (pendingRecords > 0) {
          throw new BadRequestException('存在未处理的考勤记录，无法完成考勤');
        }

        session.status = 'completed';
        session.completedBy = userId;
        session.completedAt = new Date();
        const savedSession = await sessionRepo.save(session);
        const records = await recordRepo.find({
          where: { attendanceSessionId: sessionId },
          relations: ['student'],
          order: { studentId: 'ASC' },
        });
        return { session: savedSession, records: await attachAttendanceDeviceMappings(records, this.attendanceDeviceRepo, session.classId) };
      }),
    );
  }

  async findAttendanceSession(id: number) {
    const session = await this.attendanceSessionRepo.findOne({ where: { id } });
    if (!session) throw new NotFoundException('课程考勤场次不存在');
    return session;
  }

  // ── Batch create attendance records ──
}
