/**
 * 钉钉集成服务 — 对齐 gongxue-dorm-sys
 *
 * 提供:
 * - OAuth2 access_token（新版 API + 缓存）
 * - 用户同步（自动建 Student + StudentDingMapping）
 */
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { Student } from '../entities/student.entity';
import { StudentDingMapping } from '../entities/student-ding-mapping.entity';
import { syncDingTalkStudents } from './dingtalk-student-sync';
import { IntegrationConfigService } from './config/integration-config.service';

// ── Types ──


interface DingTalkCredentials {
  appKey: string;
  appSecret: string;
}

interface DingTalkUserListResponse {
  errcode: number;
  errmsg: string;
  result: {
    has_more: boolean;
    next_cursor?: number;
    list: Array<{
      userid: string;
      name: string;
      mobile: string;
      dept_id_list: number[];
    }>;
  };
}


function isDingTalkUserListResponse(value: unknown): value is DingTalkUserListResponse {
  if (!value || typeof value !== 'object' || !('errcode' in value)) return false;
  if (typeof value.errcode !== 'number') return false;
  if ('errmsg' in value && typeof value.errmsg !== 'string') return false;
  if (!('result' in value) || !value.result || typeof value.result !== 'object') {
    return value.errcode !== 0;
  }
  if (!('has_more' in value.result) || typeof value.result.has_more !== 'boolean') return false;
  if (!('list' in value.result) || !Array.isArray(value.result.list)) return false;
  return value.result.list.every(
    (item) =>
      item &&
      typeof item === 'object' &&
      'userid' in item &&
      typeof item.userid === 'string' &&
      'name' in item &&
      typeof item.name === 'string' &&
      'mobile' in item &&
      typeof item.mobile === 'string' &&
      'dept_id_list' in item &&
      Array.isArray(item.dept_id_list) &&
      item.dept_id_list.every((id) => typeof id === 'number'),
  );
}

/** 钉钉打卡结果 — 对齐 dws attendance check result */
export interface DingTalkAttendanceResult {
  userId: string;
  userName: string;
  workDate: string;
  timeResult: string;
  locationResult: string;
  planCheckTime: string;
  actualCheckTime: string;
  checkId: string;
  checkType: string;
  /** 钉钉返回的打卡来源，例如 ATM / USER / BEACON。 */
  sourceType: string;
  /** 部分钉钉租户会额外返回考勤机名称或编号。 */
  deviceName?: string;
  deviceId?: string;
}

// ── 组织架构 API 类型 ──

interface DingTalkDeptListResponse {
  errcode: number;
  result?: Array<{ dept_id: number; name: string; parent_id: number }>;
}

interface DingTalkDeptGetResponse {
  errcode: number;
  result?: { name: string; parent_id: number };
}

export interface OrgDeptNode {
  id: number;
  name: string;
  parentId: number;
  children: OrgDeptNode[];
}

export interface OrgDeptNodeWithUsers extends OrgDeptNode {
  users: Array<{ userid: string; name: string; mobile: string; deptIds: number[] }>;
}

// ── 考勤排班 API 类型 ──

/** 班次卡段打卡时间 */
export interface DingTalkShiftTime {
  check_type: 'OnDuty' | 'OffDuty';
  across: number;
  check_time: string;
  begin_min?: number;
  end_min?: number;
  free_check?: boolean;
}

/** 班次卡段 */
export interface DingTalkShiftSection {
  times: DingTalkShiftTime[];
}

/** 班次配置 */
export interface DingTalkShiftSetting {
  is_flexible?: boolean;
  serious_late_minutes?: number;
  absenteeism_late_minutes?: number;
}

/** 创建/修改班次参数 */
export interface DingTalkShiftParams {
  id?: number;
  name: string;
  owner?: string;
  sections: DingTalkShiftSection[];
  setting?: DingTalkShiftSetting;
}

/** 班次摘要（查询返回） */
export interface DingTalkShiftSummary {
  id: number;
  name: string;
}

/** 考勤组成员 */
export interface DingTalkGroupMember {
  role: string;
  type: 'StaffMember' | 'DeptMember';
  user_id: string;
}

/** 创建考勤组参数 */
export interface DingTalkGroupParams {
  name: string;
  type: 'TURN';
  owner: string;
  members: DingTalkGroupMember[];
  shift_ids?: number[];
  enable_emp_select_class?: boolean;
  disable_check_without_schedule?: boolean;
  disable_check_when_rest?: boolean;
  /** 关闭外勤、定位、Wi-Fi 和手机蓝牙打卡，仅保留考勤机打卡入口 */
  attendance_machine_only?: boolean;
}

/** 修改考勤组参数 */
export interface DingTalkGroupUpdateParams extends DingTalkGroupParams {
  id: number;
}

/** 考勤组摘要（查询返回） */
export interface DingTalkGroupSummary {
  group_id: number;
  group_name: string;
  type: string;
  member_count: number;
}

/** 排班参数（单条） */
export interface DingTalkScheduleItem {
  userid: string;
  work_date: number;
  shift_id: number;
  is_rest?: boolean;
}

/** 排班查询结果 */
export interface DingTalkScheduleResult {
  userid: string;
  work_date: string;
  shift_id: number;
  is_rest: string;
  check_type: string;
  plan_check_time: string;
  group_id: number;
  id: number;
}


@Injectable()
export class DingTalkService {
  private readonly logger = new Logger(DingTalkService.name);
  private accessToken: string | null = null;
  private accessTokenCredentialKey: string | null = null;
  private tokenExpiresAt = 0;
  private apiRequestCount = 0;

  /** 钉钉 API 限流：每秒最多 20 次 */
  private static readonly RATE_LIMIT = 20;
  private static readonly MIN_INTERVAL = 1000 / DingTalkService.RATE_LIMIT;

  constructor(
    @InjectRepository(Student)
    private readonly studentRepo: Repository<Student>,
    @InjectRepository(StudentDingMapping)
    private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
    private readonly integrationConfigService?: IntegrationConfigService,
    private readonly dataSource?: DataSource,
  ) {}

  private async getCredentials(): Promise<DingTalkCredentials | null> {
    const rawConfig = await this.integrationConfigService?.getRawConfig('DINGTALK');
    const dbAppKey = typeof rawConfig?.agentId === 'string' ? rawConfig.agentId.trim() : '';
    const dbAppSecret = typeof rawConfig?.appSecret === 'string' ? rawConfig.appSecret.trim() : '';
    if (dbAppKey && dbAppSecret) {
      return { appKey: dbAppKey, appSecret: dbAppSecret };
    }

    const envAppKey = process.env.DINGTALK_APP_KEY?.trim();
    const envAppSecret = process.env.DINGTALK_APP_SECRET?.trim();
    if (envAppKey && envAppSecret) {
      return { appKey: envAppKey, appSecret: envAppSecret };
    }

    return null;
  }

  private async isConfigured(): Promise<boolean> {
    return !!(await this.getCredentials());
  }

  // ═══════════════════════════════════════════
  // Token — 对齐 gongxue-dorm-sys getAccessToken
  // ═══════════════════════════════════════════

  private async getAccessToken(): Promise<string> {
    const credentials = await this.getCredentials();
    if (!credentials) {
      throw new Error('DingTalk not configured');
    }

    const credentialKey = `${credentials.appKey}:${credentials.appSecret}`;
    if (
      this.accessToken &&
      this.accessTokenCredentialKey === credentialKey &&
      Date.now() < this.tokenExpiresAt - 60_000
    ) {
      return this.accessToken;
    }

    const res = await fetch('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials),
    });
    const body: unknown = await res.json();
    if (
      !body ||
      typeof body !== 'object' ||
      !('accessToken' in body) ||
      typeof body.accessToken !== 'string'
    ) {
      throw new Error(`钉钉 access_token 获取失败: ${JSON.stringify(body)}`);
    }
    const expireIn = 'expireIn' in body && typeof body.expireIn === 'number' ? body.expireIn : 7200;

    this.accessToken = body.accessToken;
    this.accessTokenCredentialKey = credentialKey;
    this.tokenExpiresAt = Date.now() + expireIn * 1000;
    this.logger.log('钉钉 access_token 获取成功');
    return this.accessToken;
  }

  // ═══════════════════════════════════════════
  // Users by department — 对齐 gongxue-dorm-sys getUsersByDepartment
  // ═══════════════════════════════════════════

  private async getDeptUsers(
    token: string,
    deptId: number,
  ): Promise<Array<{ userid: string; name: string; mobile: string; dept_id_list: number[] }>> {
    const all: Array<{ userid: string; name: string; mobile: string; dept_id_list: number[] }> = [];
    let cursor = 0;
    let hasMore = true;

    while (hasMore) {
      await this.rateLimit();
      let body: DingTalkUserListResponse;
      try {
        const res = await fetch(
          `https://oapi.dingtalk.com/topapi/v2/user/list?access_token=${token}`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ dept_id: deptId, cursor, size: 100 }),
          },
        );
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const rawBody: unknown = await res.json();
        if (!isDingTalkUserListResponse(rawBody)) throw new Error('钉钉返回了无效的用户列表');
        body = rawBody;
      } catch (error) {
        throw new ServiceUnavailableException(
          `获取部门 ${deptId} 用户失败: ${error instanceof Error ? error.message : String(error)}`,
        );
      }
      if (body.errcode !== 0 || !body.result) {
        throw new ServiceUnavailableException(
          `获取部门 ${deptId} 用户失败: ${body.errmsg || `errcode=${body.errcode}`}`,
        );
      }
      all.push(...body.result.list);
      hasMore = body.result.has_more;
      if (hasMore) {
        if (body.result.next_cursor === undefined || body.result.next_cursor === cursor) {
          throw new ServiceUnavailableException(`获取部门 ${deptId} 用户失败: 分页游标未前进`);
        }
        cursor = body.result.next_cursor;
      }
    }
    return all;
  }

  // ═══════════════════════════════════════════
  // Sync all — 主入口
  // ═══════════════════════════════════════════

  async syncAll(rootDeptId = 1): Promise<{
    deptCount: number;
    userCount: number;
    created: number;
    updated: number;
    conflicts: Array<{ dingUserId: string; name: string; reason: string }>;
  }> {
    if (!(await this.isConfigured())) {
      throw new ServiceUnavailableException('钉钉未配置');
    }
    if (!this.dataSource) throw new ServiceUnavailableException('数据库未初始化');

    const t0 = Date.now();
    const token = await this.getAccessToken();
    const visitedDeptIds = new Set<number>();
    const collectDeptIds = async (deptId: number): Promise<number[]> => {
      if (visitedDeptIds.has(deptId)) return [];
      visitedDeptIds.add(deptId);
      const ids: number[] = [deptId];
      for (const sub of await this.getSubDepts(token, deptId)) {
        ids.push(...(await collectDeptIds(sub.dept_id)));
      }
      return ids;
    };
    const allDeptIds = await collectDeptIds(rootDeptId);
    const users = new Map<string, { dingUserId: string; name: string; mobile?: string }>();
    for (const deptId of allDeptIds) {
      for (const user of await this.getDeptUsers(token, deptId)) {
        if (!users.has(user.userid)) {
          users.set(user.userid, {
            dingUserId: user.userid,
            name: user.name,
            mobile: user.mobile || undefined,
          });
        }
      }
    }
    const result = await this.dataSource.transaction((manager) =>
      syncDingTalkStudents(manager, [...users.values()]),
    );
    this.logger.log(
      `钉钉同步完成: ${users.size} 个用户, ${allDeptIds.length} 个部门, ` +
        `${result.created} 个新增, ${result.updated} 个更新, ${result.conflicts.length} 个冲突, ` +
        `API 请求 ${this.apiRequestCount} 次, 耗时 ${Date.now() - t0}ms`,
    );
    return {
      deptCount: allDeptIds.length,
      userCount: users.size,
      created: result.created,
      updated: result.updated,
      conflicts: result.conflicts,
    };
  }

  // ═══════════════════════════════════════════

  /** 递归获取子部门列表（含名称） */
  private async getSubDepts(token: string, deptId: number): Promise<Array<{ dept_id: number; name: string; parent_id: number }>> {
    await this.rateLimit();
    const res = await fetch(
      `https://oapi.dingtalk.com/topapi/v2/department/listsub?access_token=${token}`,
      { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dept_id: deptId }) },
    );
    if (!res.ok) throw new ServiceUnavailableException(`获取部门 ${deptId} 子部门失败: HTTP ${res.status}`);
    const body = await res.json() as DingTalkDeptListResponse;
    if (body.errcode !== 0) throw new ServiceUnavailableException(`获取部门 ${deptId} 子部门失败: errcode=${body.errcode}`);
    return body.result ?? [];
  }

  /** 获取单个部门详情 */
  private async getDeptInfo(token: string, deptId: number): Promise<{ name: string; parent_id: number }> {
    await this.rateLimit();
    const res = await fetch(
      `https://oapi.dingtalk.com/topapi/v2/department/get?access_token=${token}`,
      { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dept_id: deptId }) },
    );
    if (!res.ok) throw new ServiceUnavailableException(`获取部门 ${deptId} 失败: HTTP ${res.status}`);
    const body = await res.json() as DingTalkDeptGetResponse;
    if (body.errcode !== 0 || !body.result) throw new ServiceUnavailableException(`钉钉部门 ${deptId} 不存在或不可访问`);
    return body.result;
  }

  /** 递归构建部门树节点 */
  private async buildDeptNode(token: string, deptId: number, name: string, parentId: number): Promise<OrgDeptNode> {
    const subDepts = await this.getSubDepts(token, deptId);
    const children = await Promise.all(
      subDepts.map(sd => this.buildDeptNode(token, sd.dept_id, sd.name, sd.parent_id)),
    );
    return { id: deptId, name, parentId, children };
  }

  /** 获取钉钉组织部门树（只含部门） */
  async fetchOrgTree(rootDeptId = 1): Promise<OrgDeptNode[]> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();
    const rootInfo = await this.getDeptInfo(token, rootDeptId);
    const node = await this.buildDeptNode(token, rootDeptId, rootInfo.name, rootInfo.parent_id);
    return [node];
  }

  /** 获取钉钉组织部门树（含用户） */
  async fetchOrgTreeWithUsers(rootDeptId = 1): Promise<OrgDeptNodeWithUsers[]> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();
    const rootInfo = await this.getDeptInfo(token, rootDeptId);
    const deptTree = await this.buildDeptNode(token, rootDeptId, rootInfo.name, rootInfo.parent_id);

    const allDeptIds: number[] = [];
    const collectIds = (node: OrgDeptNode) => {
      allDeptIds.push(node.id);
      for (const child of node.children) collectIds(child);
    };
    collectIds(deptTree);

    const usersByDept = new Map<number, Array<{ userid: string; name: string; mobile: string; deptIds: number[] }>>();
    const placedUsers = new Set<string>();
    for (const deptId of allDeptIds) {
      for (const user of await this.getDeptUsers(token, deptId)) {
        if (placedUsers.has(user.userid)) continue;
        placedUsers.add(user.userid);
        if (!usersByDept.has(deptId)) usersByDept.set(deptId, []);
        usersByDept.get(deptId)!.push({
          userid: user.userid,
          name: user.name,
          mobile: user.mobile,
          deptIds: user.dept_id_list,
        });
      }
    }
    const attachUsers = (node: OrgDeptNode): OrgDeptNodeWithUsers => ({
      id: node.id,
      name: node.name,
      parentId: node.parentId,
      children: node.children.map(attachUsers),
      users: usersByDept.get(node.id) ?? [],
    });
    return [attachUsers(deptTree)];
  }


  // ═══════════════════════════════════════════
  // Rate limiting — 对齐 gongxue-dorm-sys
  // ═══════════════════════════════════════════

  private async rateLimit(): Promise<void> {
    await this.sleep(DingTalkService.MIN_INTERVAL);
    this.apiRequestCount++;
  }

  // ═══════════════════════════════════════════
  // 考勤打卡结果 — 对齐 dws attendance check result
  // ═══════════════════════════════════════════

  async fetchAttendanceResults(params: {
    startDate: string;
    endDate: string;
    userIds?: string[];
  }): Promise<DingTalkAttendanceResult[]> {
    if (!(await this.isConfigured())) throw new Error('DingTalk not configured');
    if (!params.userIds?.length) throw new Error('钉钉考勤 userIds 不能为空');
    if (params.userIds.length > 50) throw new Error('钉钉考勤单次最多查询50人');
    const token = await this.getAccessToken();

    const dateFrom = params.startDate.includes(' ') ? params.startDate : `${params.startDate} 00:00:00`;
    const dateTo = params.endDate.includes(' ') ? params.endDate : `${params.endDate} 23:59:59`;

    const body: Record<string, unknown> = {
      checkDateFrom: dateFrom,
      checkDateTo: dateTo,
    };
    body.userIds = params.userIds;

    const res = await fetch(
      `https://oapi.dingtalk.com/attendance/listRecord?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      },
    );
    const data = await res.json() as {
      errcode: number; errmsg: string;
      recordresult?: Array<{
        id: number; userId: string; workDate: number;
        userCheckTime: number; sourceType: string;
        checkType?: string; timeResult?: string;
        locationResult?: string; locationMethod?: string;
        userAddress?: string; userLongitude?: number; userLatitude?: number;
        deviceName?: string; deviceId?: string | number; deviceSN?: string | number;
        attendanceMachineName?: string; attendanceMachineId?: string | number;
      }>;
    };
    if (data.errcode !== 0) throw new Error(`钉钉考勤获取失败: ${data.errmsg}`);

    const records = data.recordresult ?? [];

    return records.map((r) => ({
      userId: r.userId,
      userName: '',
      workDate: new Date(r.workDate + 8 * 60 * 60 * 1000).toISOString().slice(0, 10),
      timeResult: r.timeResult ?? r.sourceType ?? '',
      locationResult: r.locationResult ?? r.locationMethod ?? r.userAddress ?? '',
      planCheckTime: '',
      actualCheckTime: new Date(r.userCheckTime).toISOString(),
      checkId: String(r.id),
      checkType: r.checkType ?? '',
      sourceType: r.sourceType ?? '',
      deviceName: r.deviceName ?? r.attendanceMachineName,
      deviceId: String(r.deviceId ?? r.attendanceMachineId ?? r.deviceSN ?? '') || undefined,
    }));
  }

  // ═══════════════════════════════════════════
  // 考勤排班 — 班次管理
  // ═══════════════════════════════════════════

  /** 创建或修改班次。id 不传=创建，传了=修改 */
  async upsertShift(params: DingTalkShiftParams): Promise<number> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();

    const body: Record<string, unknown> = {
      op_user_id: params.owner || 'manager',
      shift: {
        name: params.name,
        owner: params.owner,
        sections: params.sections.map((s) => ({
          times: s.times.map((t) => ({
            check_type: t.check_type,
            across: t.across,
            check_time: t.check_time,
            begin_min: t.begin_min ?? -1,
            end_min: t.end_min ?? -1,
            free_check: t.free_check ?? false,
          })),
        })),
        setting: params.setting
          ? {
              is_flexible: params.setting.is_flexible ?? false,
              serious_late_minutes: params.setting.serious_late_minutes ?? -1,
              absenteeism_late_minutes: params.setting.absenteeism_late_minutes ?? -1,
            }
          : undefined,
      },
    };
    if (params.id) (body.shift as Record<string, unknown>).id = params.id;

    await this.rateLimit();
    const res = await fetch(
      `https://oapi.dingtalk.com/topapi/attendance/shift/add?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      },
    );
    const data = (await res.json()) as {
      errcode: number; errmsg: string;
      result?: { id: number; name: string };
    };
    if (data.errcode !== 0) {
      throw new Error(`钉钉班次操作失败: ${data.errmsg} (code=${data.errcode})`);
    }
    this.logger.log(`钉钉班次 ${params.id ? '更新' : '创建'} 成功: ${data.result?.name} (id=${data.result?.id})`);
    return data.result!.id;
  }

  /** 查询所有班次摘要（每页最多200条） */
  async queryShifts(opUserId = 'manager'): Promise<DingTalkShiftSummary[]> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();

    const all: DingTalkShiftSummary[] = [];
    let cursor = 0;
    let hasMore = true;

    while (hasMore) {
      await this.rateLimit();
      const res = await fetch(
        `https://oapi.dingtalk.com/topapi/attendance/shift/list?access_token=${token}`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ op_user_id: opUserId, cursor }),
        },
      );
      const data = (await res.json()) as {
        errcode: number;
        errmsg: string;
        result?: {
          cursor?: number;
          has_more?: boolean;
          result?: Array<{ id: number; name: string }>;
        };
      };
      if (data.errcode !== 0) {
        throw new Error(`钉钉查询班次失败: ${data.errmsg} (code=${data.errcode})`);
      }

      const page = data.result;
      all.push(...(page?.result ?? []).map((s) => ({ id: s.id, name: s.name })));
      hasMore = page?.has_more ?? false;
      if (hasMore) {
        if (page?.cursor === undefined || page.cursor === cursor) {
          throw new Error('钉钉查询班次失败: 分页游标无效');
        }
        cursor = page.cursor;
      }
    }

    return all;
  }


  // ═══════════════════════════════════════════
  // 考勤排班 — 考勤组管理
  // ═══════════════════════════════════════════

  /** 创建排班制考勤组 */
  async createAttendanceGroup(params: DingTalkGroupParams): Promise<number> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();

    const topGroup = this.buildAttendanceGroupBody(params);

    const body = { op_user_id: params.owner, top_group: topGroup };

    await this.rateLimit();
    const res = await fetch(
      `https://oapi.dingtalk.com/topapi/attendance/group/add?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      },
    );
    const data = (await res.json()) as {
      errcode: number; errmsg: string;
      result?: { id: number };
    };
    if (data.errcode !== 0) {
      throw new Error(`钉钉创建考勤组失败: ${data.errmsg} (code=${data.errcode})`);
    }
    this.logger.log(`钉钉考勤组创建成功: ${params.name} (id=${data.result?.id})`);
    return data.result!.id;
  }

  /** 更新排班制考勤组，确保复用考勤组时同步最新打卡限制 */
  async updateAttendanceGroup(params: DingTalkGroupUpdateParams): Promise<void> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();
    const topGroup = { ...this.buildAttendanceGroupBody(params), id: params.id };

    await this.rateLimit();
    const res = await fetch(
      `https://oapi.dingtalk.com/topapi/attendance/group/modify?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ op_user_id: params.owner, top_group: topGroup }),
      },
    );
    const data = (await res.json()) as {
      errcode?: number;
      errmsg?: string;
      success?: boolean;
      message?: string;
    };
    const succeeded = data.success === true || data.errcode === 0;
    if (!succeeded) {
      throw new Error(
        `钉钉更新考勤组失败: ${data.message || data.errmsg || '未知错误'} ` +
          `(code=${data.errcode ?? 'unknown'})`,
      );
    }
    this.logger.log(`钉钉考勤组更新成功: ${params.name} (id=${params.id})`);
  }

  private buildAttendanceGroupBody(params: DingTalkGroupParams): Record<string, unknown> {
    const machineOnly = params.attendance_machine_only ?? false;
    const topGroup: Record<string, unknown> = {
      name: params.name,
      type: params.type,
      owner: params.owner,
      members: params.members.map((m) => ({
        role: m.role,
        type: m.type,
        user_id: m.user_id,
      })),
      enable_emp_select_class: machineOnly ? false : (params.enable_emp_select_class ?? true),
      disable_check_without_schedule: machineOnly ? true : (params.disable_check_without_schedule ?? false),
      disable_check_when_rest: params.disable_check_when_rest ?? true,
    };
    if (params.shift_ids?.length) {
      topGroup.shift_vo_list = params.shift_ids.map((id) => ({ id }));
    }
    if (machineOnly) {
      Object.assign(topGroup, {
        enable_outside_check: false,
        enable_position_ble: false,
        positions: [],
        wifis: [],
      });
    }
    return topGroup;
  }

  /** 查询所有考勤组摘要（分页，每页10条） */
  async queryAttendanceGroups(_opUserId = 'manager'): Promise<DingTalkGroupSummary[]> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();

    const all: DingTalkGroupSummary[] = [];
    let offset = 0;
    let hasMore = true;

    while (hasMore) {
      await this.rateLimit();
      const res = await fetch(
        `https://oapi.dingtalk.com/topapi/attendance/getsimplegroups?access_token=${token}`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ offset, size: 10 }),
        },
      );
      const data = (await res.json()) as {
        errcode: number; errmsg: string;
        result?: {
          has_more: boolean;
          groups: Array<{ group_id: number; group_name: string; type: string; member_count: number }>;
        };
      };
      if (data.errcode !== 0) {
        throw new Error(`钉钉查询考勤组失败: ${data.errmsg} (code=${data.errcode})`);
      }
      if (data.result?.groups) {
        all.push(...data.result.groups.map((g) => ({
          group_id: g.group_id,
          group_name: g.group_name,
          type: g.type,
          member_count: g.member_count,
        })));
      }
      hasMore = data.result?.has_more ?? false;
      offset += 10;
    }
    return all;
  }

  async deleteAttendanceGroup(groupId: number, opUserId = 'manager'): Promise<void> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();

    await this.rateLimit();
    const keyResponse = await fetch(
      `https://oapi.dingtalk.com/topapi/attendance/groups/idtokey?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ op_user_id: opUserId, group_id: groupId }),
      },
    );
    const keyData = await keyResponse.json() as {
      errcode: number;
      errmsg: string;
      result?: string;
    };
    if (keyData.errcode !== 0 || !keyData.result) {
      throw new Error(`钉钉考勤组ID转换失败: ${keyData.errmsg} (code=${keyData.errcode})`);
    }

    await this.rateLimit();
    const deleteResponse = await fetch(
      `https://oapi.dingtalk.com/topapi/attendance/group/delete?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ op_userid: opUserId, group_key: keyData.result }),
      },
    );
    const deleteData = await deleteResponse.json() as {
      errcode: number;
      errmsg: string;
      success?: boolean;
    };
    if (deleteData.errcode !== 0 || deleteData.success !== true) {
      throw new Error(`钉钉删除考勤组失败: ${deleteData.errmsg} (code=${deleteData.errcode})`);
    }
  }


  // ═══════════════════════════════════════════
  // 考勤排班 — 排班分配
  // ═══════════════════════════════════════════

  /** 批量排班（单次最多200条） */
  async scheduleUsers(
    groupId: number, schedules: DingTalkScheduleItem[], opUserId = 'manager',
  ): Promise<void> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    if (schedules.length === 0) return;
    if (schedules.length > 200) {
      throw new Error(`排班单次最多200条，当前 ${schedules.length} 条`);
    }

    const token = await this.getAccessToken();
    const body = {
      op_user_id: opUserId,
      group_id: groupId,
      schedules: schedules.map((s) => ({
        userid: s.userid,
        work_date: s.work_date,
        shift_id: s.shift_id,
        is_rest: s.is_rest ?? false,
      })),
    };

    await this.rateLimit();
    const res = await fetch(
      `https://oapi.dingtalk.com/topapi/attendance/group/schedule/async?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      },
    );
    const data = (await res.json()) as {
      errcode: number; errmsg: string;
    };
    if (data.errcode !== 0) {
      throw new Error(`钉钉排班失败: ${data.errmsg} (code=${data.errcode})`);
    }
    this.logger.log(`钉钉排班成功: groupId=${groupId}, ${schedules.length} 条`);
  }

  /** 查询指定用户的排班信息（7天内，最多50人） */
  async queryScheduleByUsers(
    userIds: string[], fromDate: number, toDate: number, opUserId = 'manager',
  ): Promise<DingTalkScheduleResult[]> {
    if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
    const token = await this.getAccessToken();

    await this.rateLimit();
    const res = await fetch(
      `https://oapi.dingtalk.com/topapi/attendance/schedule/listbyusers?access_token=${token}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          op_user_id: opUserId,
          userids: userIds.join(','),
          from_date_time: fromDate,
          to_date_time: toDate,
        }),
      },
    );
    const data = (await res.json()) as {
      errcode: number; errmsg: string;
      result?: Array<{
        userid: string; work_date: string; shift_id: number;
        is_rest: string; check_type: string; plan_check_time: string;
        group_id: number; id: number;
      }>;
    };
    if (data.errcode !== 0) {
      throw new Error(`钉钉查询排班失败: ${data.errmsg} (code=${data.errcode})`);
    }
    return (data.result ?? []).map((r) => ({
      userid: r.userid,
      work_date: r.work_date,
      shift_id: r.shift_id,
      is_rest: r.is_rest,
      check_type: r.check_type,
      plan_check_time: r.plan_check_time,
      group_id: r.group_id,
      id: r.id,
    }));
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}
