import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ClassSchedule, ClassStudent, StudentDingMapping, Class } from '../entities';
import { DingTalkService } from '../integration/dingtalk.service';
import {
  buildClassDingUserMap,
  loadClassNames,
  buildDailySchedulePlans,
  expandDailySchedulePlans,
  toMinutes,
  minutesBetween,
  addDays,
  type DailySchedulePeriod,
  type DailySchedulePlan,
  type ScheduleSyncResult,
} from './schedule-sync.helpers';

@Injectable()
export class ScheduleSyncService {
  private readonly logger = new Logger(ScheduleSyncService.name);

  constructor(
    @InjectRepository(ClassSchedule)
    private readonly scheduleRepo: Repository<ClassSchedule>,
    @InjectRepository(ClassStudent)
    private readonly classStudentRepo: Repository<ClassStudent>,
    @InjectRepository(StudentDingMapping)
    private readonly mappingRepo: Repository<StudentDingMapping>,
    @InjectRepository(Class)
    private readonly classRepo: Repository<Class>,
    private readonly dingTalkService: DingTalkService,
  ) {}

  /**
   * 全量同步：将所有活跃排课同步到钉钉排班。
   * @param dateFrom 起始日期（YYYY-MM-DD），默认今天
   * @param days 同步天数，默认 30
   * @param opUserId 钉钉操作人 userId
   * @param attendanceMachineOnly 是否关闭手机类打卡入口，仅使用考勤机
   */
  async syncAll(
    dateFrom?: string,
    days = 30,
    opUserId = 'manager',
    attendanceMachineOnly = false,
  ): Promise<ScheduleSyncResult> {
    const startDate = dateFrom || new Date().toISOString().slice(0, 10);
    const normalizedDays = Number.isFinite(days) ? Math.max(1, Math.floor(days)) : 30;
    const endDate = addDays(startDate, normalizedDays - 1);

    const empty: ScheduleSyncResult = {
      scheduleCount: 0,
      shiftCount: 0,
      groupCount: 0,
      syncedItems: 0,
      skippedNoMapping: 0,
      failedBatchCount: 0,
      failedItems: 0,
      errors: [],
      groups: [],
    };

    // ── Step 1: 查询活跃排课（必须关联到班级才能取学生） ──
    const allSchedules = await this.scheduleRepo.find({
      where: { status: 'active' },
    });
    const schedules = allSchedules.filter((s) => s.classId != null);
    if (schedules.length === 0) {
      this.logger.log('没有需要同步的活跃排课（无关联班级）');
      return empty;
    }

    // ── Step 2: 班级 → 学生钉钉ID 映射 ──
    const classIds = [...new Set(schedules.map((s) => s.classId as number))];
    const classDingUsers = await buildClassDingUserMap(this.classStudentRepo, this.mappingRepo, classIds);
    const classNameMap = await loadClassNames(this.classRepo, classIds);

    // ── Step 3: 将每天的多节课合并成一个钉钉班次 ──
    // 钉钉要求每人每天只能写入一条排班，因此同一天的多节课必须作为
    // 同一个班次的多个 sections 写入，不能拆成多条 schedule item。
    const dailyPlans = buildDailySchedulePlans(schedules, startDate, endDate);
    const uniqueShifts = new Map<
      string,
      { className: string; periods: DailySchedulePeriod[] }
    >();
    const shiftPlanCount = new Map<string, number>();
    for (const plan of dailyPlans) {
      if (!uniqueShifts.has(plan.shiftKey)) {
        uniqueShifts.set(plan.shiftKey, {
          className: classNameMap.get(plan.classId) || `班级${plan.classId}`,
          periods: plan.periods,
        });
      }
      shiftPlanCount.set(plan.shiftKey, (shiftPlanCount.get(plan.shiftKey) || 0) + 1);
    }

    const existingShifts = await this.dingTalkService.queryShifts(opUserId);
    const shiftByName = new Map(existingShifts.map((shift) => [shift.name, shift.id]));
    const planToShiftId = new Map<string, number>();
    const errors: string[] = [];
    let failedBatchCount = 0;
    let failedItems = 0;
    let shiftCount = 0;
    for (const [key, { className, periods }] of uniqueShifts) {
      const periodLabel = periods
        .map((period) => `${period.startTime}-${period.endTime}`)
        .join('+');
      const shiftName = `${className}_${periodLabel}`;
      try {
        let shiftId = shiftByName.get(shiftName);
        const shiftParams = {
          ...(shiftId === undefined ? {} : { id: shiftId }),
          name: shiftName,
          owner: opUserId,
          sections: periods.map((period) => ({
            times: [
              {
                check_type: 'OnDuty' as const,
                across: 0,
                check_time: `1970-01-01 ${period.startTime}:00`,
                free_check: false,
              },
              {
                check_type: 'OffDuty' as const,
                across: toMinutes(period.endTime) <= toMinutes(period.startTime) ? 1 : 0,
                check_time: `1970-01-01 ${period.endTime}:00`,
                free_check: false,
              },
            ],
          })),
          setting: {
            is_flexible: false,
            serious_late_minutes: -1,
            absenteeism_late_minutes: Math.max(
              ...periods.map((period) => minutesBetween(period.startTime, period.endTime)),
            ),
          },
        };
        shiftId = await this.dingTalkService.upsertShift(shiftParams);
        shiftByName.set(shiftName, shiftId);
        planToShiftId.set(key, shiftId);
        shiftCount++;
      } catch (e) {
        const msg = `创建班次 ${shiftName} 失败: ${(e as Error).message}`;
        this.logger.error(msg);
        errors.push(msg);
        failedBatchCount++;
        failedItems += shiftPlanCount.get(key) || 0;
      }
    }

    // ── Step 4: 考勤组列表只查一次，供每个班级匹配 ──
    const existingGroups = await this.dingTalkService.queryAttendanceGroups(opUserId);
    const groupByName = new Map(existingGroups.map((g) => [g.group_name, g.group_id]));

    // ── Step 5: 按班级同步 ──
    const schedulesByClass = new Map<number, ClassSchedule[]>();
    for (const schedule of schedules) {
      const classId = schedule.classId as number;
      if (!schedulesByClass.has(classId)) schedulesByClass.set(classId, []);
      schedulesByClass.get(classId)!.push(schedule);
    }
    const dailyPlansByClass = new Map<number, DailySchedulePlan[]>();
    for (const plan of dailyPlans) {
      if (!dailyPlansByClass.has(plan.classId)) dailyPlansByClass.set(plan.classId, []);
      dailyPlansByClass.get(plan.classId)!.push(plan);
    }

    let syncedItems = 0;
    let skippedNoMapping = 0;
    let groupCount = 0;
    const groupDetails: ScheduleSyncResult['groups'] = [];
    for (const [classId, classSchedules] of schedulesByClass) {
      const className = classNameMap.get(classId) || `班级${classId}`;
      const dingUserIds = classDingUsers.get(classId) ?? [];

      if (dingUserIds.length === 0) {
        this.logger.warn(`班级 ${className} 无钉钉映射学生，跳过 ${classSchedules.length} 条排课`);
        skippedNoMapping += classSchedules.length;
        continue;
      }

      const classDailyPlans = dailyPlansByClass.get(classId) ?? [];
      // 该班级在同步日期范围内用到的合并班次
      const classShiftIds = new Set<number>();
      for (const plan of classDailyPlans) {
        const shiftId = planToShiftId.get(plan.shiftKey);
        if (shiftId) classShiftIds.add(shiftId);
      }
      if (classShiftIds.size === 0) {
        this.logger.warn(`班级 ${className} 无可用班次，跳过（班次创建已计入 failure）`);
        continue;
      }

      // 先展开排班以计算受影响条数
      const items = expandDailySchedulePlans(classDailyPlans, dingUserIds, planToShiftId);

      if (items.length === 0) {
        this.logger.warn(`班级 ${className} 无可用班次匹配，跳过`);
        skippedNoMapping += classSchedules.length;
        continue;
      }

      // 创建/匹配该班级的考勤组
      const groupName = `排课_${className}`;
      let attendanceGroupId: number;
      try {
        const cached = groupByName.get(groupName);
        const groupParams = {
          name: groupName,
          type: 'TURN' as const,
          owner: opUserId,
          members: dingUserIds.map((uid) => ({
            role: 'Attendance',
            type: 'StaffMember' as const,
            user_id: uid,
          })),
          shift_ids: [...classShiftIds],
          enable_emp_select_class: true,
          disable_check_without_schedule: false,
          disable_check_when_rest: true,
          attendance_machine_only: attendanceMachineOnly,
        };
        if (cached !== undefined) {
          attendanceGroupId = cached;
          await this.dingTalkService.updateAttendanceGroup({
            ...groupParams,
            id: attendanceGroupId,
          });
        } else {
          attendanceGroupId = await this.dingTalkService.createAttendanceGroup(groupParams);
          groupByName.set(groupName, attendanceGroupId);
        }
        groupCount++;
      } catch (e) {
        const msg = `考勤组 ${groupName} 创建/更新失败: ${(e as Error).message}`;
        this.logger.error(msg);
        errors.push(msg);
        failedBatchCount++;
        failedItems += items.length;
        continue;
      }

      // 批量写入（单次≤200）
      let classItems = 0;
      for (let i = 0; i < items.length; i += 200) {
        const batch = items.slice(i, i + 200);
        try {
          await this.dingTalkService.scheduleUsers(attendanceGroupId, batch, opUserId);
          syncedItems += batch.length;
          classItems += batch.length;
        } catch (e) {
          const msg = `排班写入失败 (groupId=${attendanceGroupId}, batch=${Math.floor(i / 200) + 1}): ${(e as Error).message}`;
          this.logger.error(msg);
          failedBatchCount++;
          failedItems += batch.length;
          errors.push(msg);
        }
      }

      groupDetails.push({ className, groupId: attendanceGroupId, itemCount: classItems });
    }

    this.logger.log(
      `排班同步完成: ${schedules.length} 条排课 → ${syncedItems} 条钉钉排班, ` +
        `${shiftCount} 班次, ${groupCount} 考勤组, 跳过 ${skippedNoMapping} 条无映射` +
        (failedBatchCount > 0 ? `, ${failedBatchCount} 批写入失败 (${failedItems} 条)` : ''),
    );

    return {
      scheduleCount: schedules.length,
      shiftCount,
      groupCount,
      syncedItems,
      skippedNoMapping,
      failedBatchCount,
      failedItems,
      errors,
      groups: groupDetails,
    };
  }

  /**
   * 构建 classId → 学生钉钉 userId 列表。
   * 一次性查询所有班级的活跃学生与钉钉映射，避免 N+1。
   */
  async getStatus(_targetDate: string): Promise<{
    activeSchedules: number;
    mappedClasses: number;
    totalClasses: number;
  }> {
    const allSchedules = await this.scheduleRepo.find({ where: { status: 'active' } });
    const schedules = allSchedules.filter((s) => s.classId != null);
    const classIds = [...new Set(schedules.map((s) => s.classId as number))];
    const classDingUsers = await buildClassDingUserMap(this.classStudentRepo, this.mappingRepo, classIds);
    const mappedClasses = [...classDingUsers.values()].filter((u) => u.length > 0).length;

    return {
      activeSchedules: schedules.length,
      mappedClasses,
      totalClasses: classIds.length,
    };
  }
}
