import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { ClassSchedule, ClassStudent, StudentDingMapping, Class } from '../entities';
import { DingTalkService, DingTalkScheduleItem } from '../integration/dingtalk.service';

interface DailySchedulePeriod {
  startTime: string;
  endTime: string;
  scheduleId: number;
}

interface DailySchedulePlan {
  classId: number;
  date: string;
  shiftKey: string;
  periods: DailySchedulePeriod[];
}

/** 单次排班同步的结果 */
export interface ScheduleSyncResult {
  /** 参与同步的排课记录数 */
  scheduleCount: number;
  /** 创建/复用的班次数 */
  shiftCount: number;
  /** 创建/复用的考勤组数 */
  groupCount: number;
  /** 实际写入钉钉的排班条数 */
  syncedItems: number;
  /** 因无学生或无钉钉映射而跳过的排课数 */
  skippedNoMapping: number;
  /** 写入失败的排班批次数 */
  failedBatchCount: number;
  /** 写入失败的排班条数 */
  failedItems: number;
  /** 失败批次错误详情 */
  errors: string[];
  /** 按班级分组的详情 */
  groups: Array<{
    className: string;
    groupId: number;
    itemCount: number;
  }>;
}

/**
 * 排班同步服务 — 将本地 ClassSchedule 同步到钉钉考勤排班。
 *
 * ## 同步流程（按班级学生）
 * 1. 查询活跃排课，按 classId 分组
 * 2. 通过 ClassStudent + StudentDingMapping 拿到每个班级学生的钉钉 userId
 * 3. 按 (startTime, endTime) 创建/匹配钉钉班次（班次列表只拉一次）
 * 4. 每个班级创建/匹配一个排班制考勤组（考勤组列表只拉一次）
 * 5. 将排课展开为每个学生的每日排班，批量写入钉钉
 *
 * ## 残余风险：同步窗口内已不存在的旧排班无法清理
 * 钉钉开放平台未暴露排班删除接口（仅提供 `schedule/listbyusers` 查询和
 * `group/schedule/async` 写入）。`queryScheduleByUsers` 受限于 7 天窗口
 * 和每次 50 个用户，且无配套删除能力，无法在同步前清理旧排班。
 * 当前产品流程为"排课后手动同步钉钉"，依赖运营人员知晓同步时机；
 * 若后续需要自动清理，需等钉钉开放排班删除 API 或改用考勤组覆盖策略。
 *
 * ## API 调用优化
 * - 班次列表、考勤组列表各只查询一次，在内存中按名称匹配，避免每次 findOrCreate 都发一次查询。
 * - 排班写入按考勤组分批（钉钉单次最多 200 条）。
 */
@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 = this.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 this.buildClassDingUserMap(classIds);
    const classNameMap = await this.loadClassNames(classIds);

    // ── Step 3: 将每天的多节课合并成一个钉钉班次 ──
    // 钉钉要求每人每天只能写入一条排班，因此同一天的多节课必须作为
    // 同一个班次的多个 sections 写入，不能拆成多条 schedule item。
    const dailyPlans = this.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: this.toMinutes(period.endTime) <= this.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) => this.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 = this.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。
   */
  private async buildClassDingUserMap(classIds: number[]): Promise<Map<number, string[]>> {
    const result = new Map<number, string[]>();
    if (classIds.length === 0) return result;

    // 班级 → 活跃学生
    const links = await this.classStudentRepo.find({
      where: { classId: In(classIds), status: 'active' },
    });
    if (links.length === 0) return result;

    // 学生 → 钉钉 userId
    const studentIds = [...new Set(links.map((l) => l.studentId))];
    const mappings = await this.mappingRepo.find({
      where: { studentId: In(studentIds) },
    });
    const studentToDing = new Map(mappings.map((m) => [m.studentId, m.dingUserId]));

    for (const link of links) {
      const dingId = studentToDing.get(link.studentId);
      if (!dingId) continue;
      if (!result.has(link.classId)) result.set(link.classId, []);
      const arr = result.get(link.classId)!;
      if (!arr.includes(dingId)) arr.push(dingId);
    }
    return result;
  }

  private async loadClassNames(classIds: number[]): Promise<Map<number, string>> {
    const map = new Map<number, string>();
    if (classIds.length === 0) return map;
    const classes = await this.classRepo.find({ where: { id: In(classIds) } });
    for (const c of classes) map.set(c.id, c.name);
    return map;
  }

  /**
   * 把本地排课转换为“班级 + 日期”的日排班计划。
   * 同一天相同时间段会去重，多节课按开始时间排序并合并为一个钉钉班次。
   */
  private buildDailySchedulePlans(
    schedules: ClassSchedule[],
    syncFrom: string,
    syncTo: string,
  ): DailySchedulePlan[] {
    const periodMapByClassDate = new Map<string, Map<string, DailySchedulePeriod>>();
    const fromDate = new Date(`${syncFrom}T00:00:00.000Z`);
    const toDate = new Date(`${syncTo}T00:00:00.000Z`);

    for (let date = new Date(fromDate); date <= toDate; date.setUTCDate(date.getUTCDate() + 1)) {
      const dateStr = date.toISOString().slice(0, 10);
      const weekDay = date.getUTCDay() === 0 ? 7 : date.getUTCDay();

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

        const classDateKey = `${schedule.classId}|${dateStr}`;
        if (!periodMapByClassDate.has(classDateKey)) {
          periodMapByClassDate.set(classDateKey, new Map());
        }
        const periods = periodMapByClassDate.get(classDateKey)!;
        const periodKey = `${schedule.startTime}-${schedule.endTime}`;
        const existing = periods.get(periodKey);
        if (!existing || schedule.id < existing.scheduleId) {
          periods.set(periodKey, {
            startTime: schedule.startTime,
            endTime: schedule.endTime,
            scheduleId: schedule.id,
          });
        }
      }
    }

    const plans: DailySchedulePlan[] = [];
    for (const [classDateKey, periodMap] of periodMapByClassDate) {
      const separator = classDateKey.indexOf('|');
      const classId = Number(classDateKey.slice(0, separator));
      const date = classDateKey.slice(separator + 1);
      const periods = [...periodMap.values()].sort(
        (left, right) =>
          left.startTime.localeCompare(right.startTime) ||
          left.endTime.localeCompare(right.endTime) ||
          left.scheduleId - right.scheduleId,
      );
      const periodSignature = periods
        .map((period) => `${period.startTime}-${period.endTime}`)
        .join('+');
      plans.push({
        classId,
        date,
        shiftKey: `${classId}|${periodSignature}`,
        periods,
      });
    }

    return plans.sort(
      (left, right) => left.date.localeCompare(right.date) || left.classId - right.classId,
    );
  }

  /** 每个学生每天仅生成一条钉钉排班，shift 内可包含多个课程卡段。 */
  private expandDailySchedulePlans(
    plans: DailySchedulePlan[],
    dingUserIds: string[],
    planToShiftId: Map<string, number>,
  ): DingTalkScheduleItem[] {
    const items: DingTalkScheduleItem[] = [];
    for (const plan of plans) {
      const shiftId = planToShiftId.get(plan.shiftKey);
      if (!shiftId) continue;
      const workDate = new Date(`${plan.date}T00:00:00+08:00`).getTime();
      for (const userid of dingUserIds) {
        items.push({ userid, work_date: workDate, shift_id: shiftId, is_rest: false });
      }
    }
    return items;
  }

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

  private minutesBetween(startTime: string, endTime: string): number {
    const start = this.toMinutes(startTime);
    let end = this.toMinutes(endTime);
    if (end <= start) end += 24 * 60;
    return end - start;
  }

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

  /** 获取排班同步状态：活跃排课数、有钉钉映射学生的班级数 */
  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 this.buildClassDingUserMap(classIds);
    const mappedClasses = [...classDingUsers.values()].filter((u) => u.length > 0).length;

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