import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, DataSource } from 'typeorm';
import {
  AttendanceRecord,
  AttendanceSession,
  AttendanceDevice,
  AttendancePeriodConfig,
  DingAttendanceRaw,
  DingLeaveRaw,
  Class,
  Student,
  ClassSchedule,
  ClassStudent,
  ClassTeacher,
  StudentDingMapping,
} from '../entities';
import { AttendanceQueryService } from './attendance-query.service';
import { AttendanceLessonService } from './attendance-lesson.service';
import { AttendanceGenerationService } from './attendance-generation.service';
import { SessionMutex } from './attendance-mutex';

interface AgentAttendanceSummaryRow {
  date: string;
  classId: string | number;
  className: string;
  status: string;
  count: string | number;
}

/** Keyed mutex serializing operations on the same attendance session. */
@Injectable()
export class AttendanceService {
  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>,
    @InjectRepository(AttendancePeriodConfig)
    private attendancePeriodConfigRepo: Repository<AttendancePeriodConfig>,
    private dataSource: DataSource,
  ) {}

  private sessionMutex = new SessionMutex();



  private queryService?: AttendanceQueryService;
  private lessonService?: AttendanceLessonService;
  private generationService?: AttendanceGenerationService;

  private get lessons(): AttendanceLessonService {
    if (!this.lessonService) {
      this.lessonService = new AttendanceLessonService(
        this.attendanceRepo,
        this.dingRawRepo,
        this.dingLeaveRawRepo,
        this.classRepo,
        this.studentRepo,
        this.scheduleRepo,
        this.classStudentRepo,
        this.studentDingMappingRepo,
        this.classTeacherRepo,
        this.attendanceSessionRepo,
        this.attendanceDeviceRepo,
        this.dataSource,
      );
    }
    return this.lessonService;
  }

  private get generation(): AttendanceGenerationService {
    if (!this.generationService) {
      this.generationService = new AttendanceGenerationService(
        this.attendanceRepo,
        this.scheduleRepo,
        this.classRepo,
        this.classStudentRepo,
        this.attendanceSessionRepo,
        this.attendancePeriodConfigRepo,
        this.dataSource,
      );
    }
    return this.generationService;
  }

  private get queries(): AttendanceQueryService {
    if (!this.queryService) {
      this.queryService = new AttendanceQueryService(
        this.attendanceRepo,
        this.dingRawRepo,
        this.classRepo,
        this.scheduleRepo,
        this.classStudentRepo,
        this.studentDingMappingRepo,
        this.classTeacherRepo,
        this.attendanceDeviceRepo,
        this.dataSource,
      );
    }
    return this.queryService;
  }

  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 BadRequestException('只能访问自己任教班级的考勤');
  }

  async agentGetAttendanceSummary(
    userId: number,
    canManageAll: boolean,
    query: { classId?: number; dateFrom?: string; dateTo?: string; limit?: number },
  ) {
    const accessibleClassIds = await this.getAccessibleClassIds(userId, canManageAll);
    if (accessibleClassIds?.length === 0) return [];
    if (query.classId && accessibleClassIds && !accessibleClassIds.includes(query.classId)) return [];

    const qb = this.attendanceRepo
      .createQueryBuilder('attendance')
      .leftJoin('attendance.class', 'class')
      .select('attendance.attendanceDate', 'date')
      .addSelect('attendance.classId', 'classId')
      .addSelect('class.name', 'className')
      .addSelect('attendance.status', 'status')
      .addSelect('COUNT(attendance.id)', 'count')
      .where('attendance.classId IS NOT NULL');
    if (query.classId) qb.andWhere('attendance.classId = :classId', { classId: query.classId });
    else if (accessibleClassIds) qb.andWhere('attendance.classId IN (:...accessibleClassIds)', { accessibleClassIds });
    if (query.dateFrom) qb.andWhere('attendance.attendanceDate >= :dateFrom', { dateFrom: query.dateFrom });
    if (query.dateTo) qb.andWhere('attendance.attendanceDate <= :dateTo', { dateTo: query.dateTo });
    const rows = await qb
      .groupBy('attendance.attendanceDate')
      .addGroupBy('attendance.classId')
      .addGroupBy('class.name')
      .addGroupBy('attendance.status')
      .orderBy('attendance.attendanceDate', 'DESC')
      .addOrderBy('class.name', 'ASC')
      .limit(query.limit ?? 30)
      .getRawMany<AgentAttendanceSummaryRow>();
    return rows.map((row) => ({ ...row, classId: Number(row.classId), count: Number(row.count || 0) }));
  }

  async getImportableClasses(userId: number, isSuperAdmin = false) {
    if (isSuperAdmin) {
      const classes = await this.classRepo.find({
        where: { isArchived: false },
        order: { name: 'ASC' },
      });
      return classes.map((item) => ({ classId: item.id, className: item.name }));
    }

    const assignments = await this.classTeacherRepo.find({
      where: { userId },
      relations: ['class'],
    });
    const classes = new Map<number, string>();
    for (const assignment of assignments) {
      if (assignment.class && !assignment.class.isArchived) {
        classes.set(assignment.classId, assignment.class.name);
      }
    }
    return [...classes.entries()]
      .map(([classId, className]) => ({ classId, className }))
      .sort((left, right) => left.className.localeCompare(right.className, 'zh-CN'));
  }

  /** Resolve the DingTalk users a teacher may import for one assigned class. */
  async getTeacherClassDingUserIds(
    userId: number,
    classId: number,
    isSuperAdmin = false,
    lessonDate?: string,
  ): Promise<string[]> {
    if (!isSuperAdmin) {
      const assignment = await this.classTeacherRepo.findOne({
        where: { userId, classId },
      });
      if (!assignment) {
        throw new BadRequestException('只能拉取自己任教班级的考勤记录');
      }
    } else {
      const cls = await this.classRepo.findOne({ where: { id: classId } });
      if (!cls) throw new NotFoundException(`Class ${classId} not found`);
    }

    const classStudents = lessonDate
      ? await this.lessons.getClassStudentsForLesson(classId, lessonDate)
      : await this.classStudentRepo.find({
          where: { classId, status: 'active' },
        });
    const studentIds = [...new Set(classStudents.map((item) => item.studentId))];
    if (studentIds.length === 0) {
      throw new BadRequestException('该班级暂无在读学生');
    }

    const mappings = await this.studentDingMappingRepo.find({
      where: { studentId: In(studentIds) },
    });
    const userIds = [...new Set(mappings.map((mapping) => mapping.dingUserId).filter(Boolean))];
    if (userIds.length === 0) {
      throw new BadRequestException('该班级学生尚未同步钉钉账号');
    }
    return userIds.sort();
  }


  async getSummary(...args: Parameters<AttendanceQueryService['getSummary']>) {
    return this.queries.getSummary(...args);
  }

  async getCalendar(...args: Parameters<AttendanceQueryService['getCalendar']>) {
    return this.queries.getCalendar(...args);
  }

  async getScheduleOptionsForAttendance(
    ...args: Parameters<AttendanceQueryService['getScheduleOptionsForAttendance']>
  ) {
    return this.queries.getScheduleOptionsForAttendance(...args);
  }

  async findAll(...args: Parameters<AttendanceQueryService['findAll']>) {
    return this.queries.findAll(...args);
  }

  async getClasses(...args: Parameters<AttendanceQueryService['getClasses']>) {
    return this.queries.getClasses(...args);
  }

  async getDingRaw(...args: Parameters<AttendanceQueryService['getDingRaw']>) {
    return this.queries.getDingRaw(...args);
  }

  async matchDingRecord(...args: Parameters<AttendanceQueryService['matchDingRecord']>) {
    return this.queries.matchDingRecord(...args);
  }

  async autoMatchDingRecords(): Promise<{ matched: number; total: number }> {
    return this.queries.autoMatchDingRecords();
  }

  async findAllForExport(...args: Parameters<AttendanceQueryService['findAllForExport']>) {
    return this.queries.findAllForExport(...args);
  }

  async findAttendanceRecord(...args: Parameters<AttendanceQueryService['findAttendanceRecord']>) {
    return this.queries.findAttendanceRecord(...args);
  }

  async update(...args: Parameters<AttendanceQueryService['update']>) {
    return this.queries.update(...args);
  }

  async remove(...args: Parameters<AttendanceQueryService['remove']>) {
    return this.queries.remove(...args);
  }

  async getReport(...args: Parameters<AttendanceQueryService['getReport']>) {
    return this.queries.getReport(...args);
  }

  async getAlerts(...args: Parameters<AttendanceQueryService['getAlerts']>) {
    return this.queries.getAlerts(...args);
  }

  async getLessonAttendance(...args: Parameters<AttendanceLessonService['getLessonAttendance']>) {
    return this.lessons.getLessonAttendance(...args);
  }

  getLessonAttendanceImportDateRange(
    ...args: Parameters<AttendanceLessonService['getLessonAttendanceImportDateRange']>
  ) {
    return this.lessons.getLessonAttendanceImportDateRange(...args);
  }

  async createLessonAttendanceFromDingTalk(
    ...args: Parameters<AttendanceLessonService['createLessonAttendanceFromDingTalk']>
  ) {
    return this.lessons.createLessonAttendanceFromDingTalk(...args);
  }

  async completeLessonAttendance(
    ...args: Parameters<AttendanceLessonService['completeLessonAttendance']>
  ) {
    return this.lessons.completeLessonAttendance(...args);
  }

  async findAttendanceSession(
    ...args: Parameters<AttendanceLessonService['findAttendanceSession']>
  ) {
    return this.lessons.findAttendanceSession(...args);
  }

  async getRefreshableSchedules(
    ...args: Parameters<AttendanceGenerationService['getRefreshableSchedules']>
  ) {
    return this.generation.getRefreshableSchedules(...args);
  }

  async batchCreate(...args: Parameters<AttendanceGenerationService['batchCreate']>) {
    return this.generation.batchCreate(...args);
  }

  async generateFromSchedules(
    ...args: Parameters<AttendanceGenerationService['generateFromSchedules']>
  ) {
    return this.generation.generateFromSchedules(...args);
  }

  async getAttendancePeriodConfigs(
    ...args: Parameters<AttendanceGenerationService['getAttendancePeriodConfigs']>
  ) {
    return this.generation.getAttendancePeriodConfigs(...args);
  }

  async saveAttendancePeriodConfigs(
    ...args: Parameters<AttendanceGenerationService['saveAttendancePeriodConfigs']>
  ) {
    return this.generation.saveAttendancePeriodConfigs(...args);
  }

  async resetAttendancePeriodConfigs(
    ...args: Parameters<AttendanceGenerationService['resetAttendancePeriodConfigs']>
  ) {
    return this.generation.resetAttendancePeriodConfigs(...args);
  }
}
