// aislop-ignore-file: duplicate-block -- 钉钉 API 调用块结构相似（端点/参数不同）
/**
 * 钉钉集成服务 — 对齐 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';
import { DINGTALK_OAUTH_TOKEN_URL } from './endpoints';
import { isDingTalkUserListResponse } from './dingtalk.types';
import type {
  DingTalkCredentials,
  DingTalkDeptGetResponse,
  DingTalkDeptListResponse,
  DingTalkServiceContext,
  DingTalkUserListResponse,
  OrgDeptNode,
  OrgDeptNodeWithUsers,
} from './dingtalk.types';
import { DingTalkAttendanceClient } from './dingtalk.attendance';
import { DingTalkLeaveClient } from './dingtalk.leave';
import { DingTalkShiftClient } from './dingtalk.shifts';
import { DingTalkGroupClient } from './dingtalk.groups';
import { DingTalkScheduleClient } from './dingtalk.schedules';

export type {
  DingTalkAttendanceResult,
  DingTalkLeaveResult,
  DingTalkGroupParams,
  DingTalkGroupSummary,
  DingTalkGroupUpdateParams,
  DingTalkScheduleItem,
  DingTalkScheduleResult,
  DingTalkShiftParams,
  DingTalkShiftSummary,
  OrgDeptNode,
  OrgDeptNodeWithUsers,
} from './dingtalk.types';

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

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

  private attendanceClient?: DingTalkAttendanceClient;
  private leaveClient?: DingTalkLeaveClient;
  private shiftClient?: DingTalkShiftClient;
  private groupClient?: DingTalkGroupClient;
  private scheduleClient?: DingTalkScheduleClient;

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

  private get attendance(): DingTalkAttendanceClient {
    if (!this.attendanceClient) this.attendanceClient = new DingTalkAttendanceClient(this);
    return this.attendanceClient;
  }

  private get leaves(): DingTalkLeaveClient {
    if (!this.leaveClient) this.leaveClient = new DingTalkLeaveClient(this);
    return this.leaveClient;
  }

  private get shifts(): DingTalkShiftClient {
    if (!this.shiftClient) this.shiftClient = new DingTalkShiftClient(this);
    return this.shiftClient;
  }

  private get groups(): DingTalkGroupClient {
    if (!this.groupClient) this.groupClient = new DingTalkGroupClient(this);
    return this.groupClient;
  }

  private get schedules(): DingTalkScheduleClient {
    if (!this.scheduleClient) this.scheduleClient = new DingTalkScheduleClient(this);
    return this.scheduleClient;
  }

  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;
  }

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

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

  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(DINGTALK_OAUTH_TOKEN_URL, {
      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
  // ═══════════════════════════════════════════

  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,
    options: { createMissing?: boolean; updateProfile?: boolean } = {},
  ): Promise<{
    deptCount: number;
    userCount: number;
    created: number;
    updated: number;
    matched: number;
    skipped: 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()], options),
    );
    this.logger.log(
      `钉钉同步完成: ${users.size} 个用户, ${allDeptIds.length} 个部门, ` +
        `${result.created} 个新增, ${result.updated} 个更新, ${result.matched} 个手机号绑定, ` +
        `${result.skipped} 个跳过, ${result.conflicts.length} 个冲突, ` +
        `API 请求 ${this.apiRequestCount} 次, 耗时 ${Date.now() - t0}ms`,
    );
    return {
      deptCount: allDeptIds.length,
      userCount: users.size,
      created: result.created,
      updated: result.updated,
      matched: result.matched,
      skipped: result.skipped,
      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
  // ═══════════════════════════════════════════

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

  async fetchAttendanceResults(
    ...args: Parameters<DingTalkAttendanceClient['fetchAttendanceResults']>
  ) {
    return this.attendance.fetchAttendanceResults(...args);
  }

  async fetchDailyLeaveStatus(
    ...args: Parameters<DingTalkLeaveClient['fetchDailyLeaveStatus']>
  ) {
    return this.leaves.fetchDailyLeaveStatus(...args);
  }

  async upsertShift(...args: Parameters<DingTalkShiftClient['upsertShift']>) {
    return this.shifts.upsertShift(...args);
  }

  async queryShifts(...args: Parameters<DingTalkShiftClient['queryShifts']>) {
    return this.shifts.queryShifts(...args);
  }

  async createAttendanceGroup(
    ...args: Parameters<DingTalkGroupClient['createAttendanceGroup']>
  ) {
    return this.groups.createAttendanceGroup(...args);
  }

  async updateAttendanceGroup(
    ...args: Parameters<DingTalkGroupClient['updateAttendanceGroup']>
  ) {
    return this.groups.updateAttendanceGroup(...args);
  }

  async queryAttendanceGroups(
    ...args: Parameters<DingTalkGroupClient['queryAttendanceGroups']>
  ) {
    return this.groups.queryAttendanceGroups(...args);
  }

  async deleteAttendanceGroup(
    ...args: Parameters<DingTalkGroupClient['deleteAttendanceGroup']>
  ) {
    return this.groups.deleteAttendanceGroup(...args);
  }

  async scheduleUsers(...args: Parameters<DingTalkScheduleClient['scheduleUsers']>) {
    return this.schedules.scheduleUsers(...args);
  }

  async queryScheduleByUsers(
    ...args: Parameters<DingTalkScheduleClient['queryScheduleByUsers']>
  ) {
    return this.schedules.queryScheduleByUsers(...args);
  }

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