import { Injectable } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { DingtalkAttendanceService } from './sdk/dingtalk-attendance.service';
import { DingtalkHrmService } from './sdk/dingtalk-hrm.service';
import { DingtalkYidaService } from './sdk/dingtalk-yida.service';
import {
  getAllLeaveCodesFromCache,
  getLeaveTypeName,
  saveLeaveTypesCache,
  extractAnnualLeaveYear,
} from './constants';

type EmployeeMeta = {
  userId: string;
  name: string;
  employeeNumber: string;
};

export type AnnualLeaveQuotaItem = {
  userId: string;
  name: string;
  employeeNumber: string;
  status: string;
  leaveType: string;
  leaveCode: string;
  quotaCycle: string;
  totalDays: number;
  usedDays: number;
  remainingDays: number;
  startTime: string | null;
  endTime: string | null;
  localPlanTotalDays?: number | null;
  localPlanNotCountDays?: number | null;
  localPlanReleasedByToday?: number | null;  // 截至今天应释放天数（过去年份=totalDays，当前年份=已到期数，未来年份=0）
};

export type EmployeeStatusInfo = {
  userId: string;
  periodCount: number;          // employment_periods 段数（>1 表示离职再入职）
  hasActiveSuspension: boolean; // 当前停薪中
  hasAnySuspension: boolean;    // 历史存在停薪记录
};

export type AnnualLeaveReleasePlanItem = {
  date: string;
  userId: string;
  name: string;
  employeeNumber: string;
  status: string;
  days: number;
  released: boolean;
  year: string;
  hasPlan: boolean;
  noPlanReason?: string | null;
};

type EmployeeInfoRow = {
  userId: string;
  name: string;
  employeeNumber: string;
  status: string;
  joinDate: string;
  workStartDate: string;
};

export type AnnualLeaveQuotaOverview = {
  lastSyncedAt: string | null;
  summary: {
    employeeCount: number;
    leaveTypeCount: number;
    nonZeroQuotaCount: number;
  };
  allLeaveTypes: { leaveCode: string; leaveType: string; hasQuota: boolean }[];
  allEmployees: { userId: string; name: string; employeeNumber: string }[];
  employeeStatuses: EmployeeStatusInfo[];
  items: AnnualLeaveQuotaItem[];
};

export type AnnualLeaveQuotaRefreshResult = {
  employeeCount: number;
  recordCount: number;
  leaveTypeCount: number;
  lastSyncedAt: string;
};

export type AnnualLeaveQuotaUsageRecord = {
  date: string | null;
  startTime: string | null;
  endTime: string | null;
  days: number;
  status: string;
  recordType: string;
  operationDate: string | null;
};

export type AnnualLeaveQuotaReleaseRecord = {
  date: string;
  days: number;
  released: boolean;
  year: string;
};

export type AnnualLeaveQuotaDetail = {
  userId: string;
  name: string;
  employeeNumber: string;
  leaveType: string;
  leaveCode: string;
  quotaCycle: string;
  totalDays: number;
  usedDays: number;
  remainingDays: number;
  startTime: string | null;
  endTime: string | null;
  usageRecords: AnnualLeaveQuotaUsageRecord[];
  releaseRecords: AnnualLeaveQuotaReleaseRecord[];
  localPlanTotalDays: number | null;
  localPlanNotCountDays: number | null;
  localPlanAdjustmentDays: number | null;
  localPlanReleasedByToday: number | null;
  tenureDays: number;
  workStartDate: string | null;
  employmentPeriods: Array<{
    periodIndex: number;
    joinDate: string;
    leaveDate: string | null;
    countInTenure: boolean;
    note: string;
  }>;
  suspensionPeriods: Array<{
    startDate: string;
    endDate: string | null;
    reason: string;
    note: string;
  }>;
};

type LeaveQuotaSnapshotRow = {
  userId: string;
  employeeName: string;
  employeeNumber: string;
  leaveType: string;
  leaveCode: string;
  quotaCycle: string;
  totalDays: unknown;
  usedDays: unknown;
  remainingDays: unknown;
  startDate: Date | null;
  endDate: Date | null;
  snapshotAt: Date;
};

type AnnualLeaveReleasePlanRow = {
  userId: string;
  year: number;
  adjustmentDays: unknown;
  notCountDays: number;
  totalDays: number;
  releaseSchedule: unknown;
  lastCalculatedAt: Date;
};

@Injectable()
export class AnnualLeaveInsightService {
  constructor(
    private prisma: PrismaService,
    private attendanceService: DingtalkAttendanceService,
    private hrmService: DingtalkHrmService,
    private yidaService: DingtalkYidaService,
  ) {}

  /**
   * 从钉钉 API 拉取企业所有假期类型，写入 JSON 缓存。
   */
  async refreshLeaveTypes(): Promise<{ count: number; leaveTypes: Array<{ leave_code: string; leave_name: string }> }> {
    const types = await this.attendanceService.listLeaveTypes();
    const leaveTypes = types.map((t) => ({ leave_code: t.leave_code, leave_name: t.leave_name }));
    saveLeaveTypesCache(leaveTypes);
    return { count: leaveTypes.length, leaveTypes };
  }

  async refreshQuotaSnapshot(params?: {
    userId?: string;
  }): Promise<AnnualLeaveQuotaRefreshResult> {
    const employees = await this.loadEmployees(params?.userId);
    const employeeIds = employees.map((item) => item.userId);
    const employeeMap = new Map(employees.map((item) => [item.userId, item]));
    const snapshotAt = new Date();
    const records: Array<Record<string, unknown>> = [];

    // 假期类型：优先用缓存，缓存为空时才拉取
    let leaveCodes = getAllLeaveCodesFromCache();
    if (leaveCodes.length === 0) {
      try {
        await this.refreshLeaveTypes();
        leaveCodes = getAllLeaveCodesFromCache();
      } catch {
        // 拉取失败，继续用硬编码
      }
    }
    for (const leaveCode of leaveCodes) {
      const quotas = await this.attendanceService.searchLeaveQuota(
        leaveCode,
        employeeIds,
      );

      // 钉钉返回的有配额的员工
      const quotaByUser = new Map<string, any>();
      for (const quota of quotas) {
        const uid = String(quota.userid || '').trim();
        quotaByUser.set(uid, quota);
      }

      // 批量拉取消费记录（钉钉 API 支持 userids 逗号分隔，分批每批 20 人）
      const targetEmployees = params?.userId
        ? employees.filter(e => e.userId === params.userId)
        : employees;

      // 新版 API v1.0：字段用驼峰命名，支持 gmtCreate/opUserId
      const BATCH_SIZE = 50; // 钉钉 API userIds 上限 50 人
      const usageByUser = new Map<string, any[]>();
      const releaseByUser = new Map<string, any[]>();
      for (let i = 0; i < targetEmployees.length; i += BATCH_SIZE) {
        const batch = targetEmployees.slice(i, i + BATCH_SIZE);
        const batchUserIds = batch.map(e => e.userId);
        try {
          const rawRecords = await this.attendanceService.getLeaveRecords(leaveCode, batchUserIds);
          for (const record of rawRecords) {
            const uid = String(record.userId || record.userid || '').trim();
            const calType = record.calType || record.cal_type;
            const recordType = record.leaveRecordType || record.leave_record_type;
            const status = record.leaveStatus || record.leave_status;

            if (recordType === 'leave' && !calType) {
              // 使用记录（请假）：只保留已通过和审批中
              if (status === 'refuse' || status === 'abort' || status === 'revoke') continue;
              if (!usageByUser.has(uid)) usageByUser.set(uid, []);
              usageByUser.get(uid)!.push(this.mapUsageRecord(record));
            } else if (calType) {
              // 配额变更记录（释放/撤销/初始化/调整）
              if (!releaseByUser.has(uid)) releaseByUser.set(uid, []);
              const days = this.normalizeQuotaNumber(record.recordNumPerDay ?? record.record_num_per_day ?? 0);
              // 跳过 days=0 的记录（如 insert 初始化配额，无实际额度变更）
              if (days === 0) continue;
              const isPositive = calType === 'insert' || calType === 'add' || calType === 'update';
              const gmtCreate = record.gmtCreate || record.gmt_create;
              const operationDate = gmtCreate ? this.formatUnknownDateTime(gmtCreate)?.slice(0, 10) : null;
              const startDate = this.formatUnknownDateTime(record.startTime || record.start_time)?.slice(0, 10) || null;
              const endDate = this.formatUnknownDateTime(record.endTime || record.end_time)?.slice(0, 10) || null;
              releaseByUser.get(uid)!.push({
                date: operationDate,
                startDate,
                endDate,
                days: isPositive ? days : -days,
                reason: record.leaveReason || record.leave_reason || '',
                calType,
                recordType,
                opUserId: record.opUserId || record.op_userid || null,
                released: isPositive,
              });
            }
          }
        } catch { /* 拉取失败不阻塞快照 */ }
      }

      for (const employee of targetEmployees) {
        const quota = quotaByUser.get(employee.userId);
        const usageRecords = (usageByUser.get(employee.userId) || [])
          .filter((r: any) => !!r.date || !!r.startTime || r.days > 0);

        // 释放记录：只用钉钉 API 的配额变更记录，不 fallback 到本地计划
        // 与钉钉保持 100% 一致
        const releaseRecords = releaseByUser.get(employee.userId) || [];

        if (quota) {
          // 100% 使用钉钉 API 返回的原始值，不做二次计算
          const totalDays = this.normalizeQuotaNumber(quota.quota_num_per_day);
          const usedDays = this.normalizeQuotaNumber(quota.used_num_per_day);
          records.push({
            userId: employee.userId,
            employeeName: employee.name,
            employeeNumber: employee.employeeNumber,
            leaveCode,
            leaveType: getLeaveTypeName(leaveCode) || leaveCode,
            quotaCycle: String(quota.quota_cycle || ''),
            totalDays,
            usedDays,
            remainingDays: Math.max(0, totalDays - usedDays),
            startDate: this.parseTimestampToDate(quota.start_time),
            endDate: this.parseTimestampToDate(quota.end_time),
            usageRecords: usageRecords.length > 0 ? usageRecords : null,
            releaseRecords: releaseRecords.length > 0 ? releaseRecords : null,
            snapshotAt,
          });
        } else {
          // 钉钉无配额数据，数字全部为 0，记录仅作补充展示
          records.push({
            userId: employee.userId,
            employeeName: employee.name,
            employeeNumber: employee.employeeNumber,
            leaveCode,
            leaveType: getLeaveTypeName(leaveCode) || leaveCode,
            quotaCycle: '',
            totalDays: 0,
            usedDays: 0,
            remainingDays: 0,
            startDate: null,
            endDate: null,
            usageRecords: usageRecords.length > 0 ? usageRecords : null,
            releaseRecords: releaseRecords.length > 0 ? releaseRecords : null,
            snapshotAt,
          });
        }
      }
    }

    await this.prisma.$transaction(async (tx) => {
      const snapshotRepo = (tx as any).dingtalkLeaveQuotaSnapshot;
      if (params?.userId) {
        await snapshotRepo.deleteMany({ where: { userId: params.userId } });
      } else {
        await snapshotRepo.deleteMany();
      }

      if (records.length > 0) {
        await snapshotRepo.createMany({
          data: records as any,
        });
      }
    });

    return {
      employeeCount: employeeIds.length,
      recordCount: records.length,
      leaveTypeCount: new Set(records.map((item) => String(item.leaveCode)))
        .size,
      lastSyncedAt: snapshotAt.toISOString(),
    };
  }

  async getQuotaOverview(params?: {
    userId?: string;
    keyword?: string;
    hideZero?: boolean;
    includeAllStatuses?: boolean;
  }): Promise<AnnualLeaveQuotaOverview> {
    const where: Record<string, unknown> = {};
    if (params?.userId) {
      where.userId = params.userId;
    }
    if (params?.keyword?.trim()) {
      const keyword = params.keyword.trim();
      where.OR = [
        { employeeName: { contains: keyword } },
        { employeeNumber: { contains: keyword } },
        { userId: { contains: keyword } },
      ];
    }

    const snapshotRepo = (this.prisma as any).dingtalkLeaveQuotaSnapshot;

    // 默认只看「正常」员工；includeAllStatuses=true 时含顾问/停薪留职/已离职，用于清查残留余额
    const employeeRepo = (this.prisma as any).dingtalkEmployee;
    const employeeWhere = params?.includeAllStatuses ? {} : { status: '正常' };
    const visibleEmployees: Array<{ userId: string; name: string; employeeNumber: string; status: string }> = await employeeRepo.findMany({
      where: employeeWhere,
      select: { userId: true, name: true, employeeNumber: true, status: true },
    });
    const visibleUserIds = new Set(visibleEmployees.map((e) => e.userId));
    const statusByUserId = new Map(visibleEmployees.map((e) => [e.userId, e.status]));

    if (visibleUserIds.size > 0) {
      where.userId = { in: [...visibleUserIds] };
    }

    // 「按员工筛选」下拉始终只含正常员工，保持原有 UX 不变
    const dropdownEmployees = params?.includeAllStatuses
      ? visibleEmployees.filter((e) => e.status === '正常')
      : visibleEmployees;

    const [rows, latestSnapshot] = await Promise.all([
      snapshotRepo.findMany({
        where: where as any,
        orderBy: [{ employeeName: 'asc' }, { leaveType: 'asc' }],
      }),
      snapshotRepo.findFirst({
        orderBy: { snapshotAt: 'desc' },
      }),
    ]);

    // 预载年假本地计划，供"释放记录累计 vs 本地计划总额"对照用
    const localPlans = await this.loadLocalAnnualLeavePlans(
      (rows as LeaveQuotaSnapshotRow[]).map((r) => ({
        userId: r.userId,
        leaveType: r.leaveType,
      })),
    );

    const allItems: AnnualLeaveQuotaItem[] = (rows as (LeaveQuotaSnapshotRow & { usageRecords?: any; releaseRecords?: any })[])
      .map((row) => {
        // 从缓存记录算摘要（释放总天数、使用总天数）
        const usageArr = Array.isArray(row.usageRecords) ? row.usageRecords : [];
        const releaseArr = Array.isArray(row.releaseRecords) ? row.releaseRecords : [];
        const releasedDays = releaseArr.reduce((sum: number, r: any) => sum + (Number(r.days) || 0), 0);
        const usedFromRecords = usageArr.reduce((sum: number, r: any) => sum + (Number(r.days) || 0), 0);

        const year = extractAnnualLeaveYear(row.leaveType);
        const planKey = year ? `${row.userId}|${year}` : null;
        const plan = planKey ? localPlans.get(planKey) : undefined;

        return {
        userId: row.userId,
        name: row.employeeName,
        employeeNumber: row.employeeNumber,
        status: statusByUserId.get(row.userId) || '未知',
        leaveType: row.leaveType,
        leaveCode: row.leaveCode,
        quotaCycle: row.quotaCycle,
        totalDays: this.normalizeStoredNumber(row.totalDays),
        usedDays: this.normalizeStoredNumber(row.usedDays),
        remainingDays: this.normalizeStoredNumber(row.remainingDays),
        startTime: row.startDate ? this.formatDate(row.startDate) : null,
        endTime: row.endDate ? this.formatDate(row.endDate) : null,
        // 从缓存记录算出的摘要（补充展示用，不影响配额数字）
        releasedDays: Math.round(releasedDays * 100) / 100,
        usedFromRecords: Math.round(usedFromRecords * 100) / 100,
        localPlanTotalDays: plan?.totalDays ?? null,
        localPlanNotCountDays: plan?.notCountDays ?? null,
        localPlanReleasedByToday: plan?.releasedByToday ?? null,
      };
      });

    // 过滤掉占位记录
    const realItems = allItems.filter((item) => item.userId !== '__NO_QUOTA__');

    const items = params?.hideZero
      ? realItems.filter((item) => item.totalDays !== 0 || item.remainingDays !== 0)
      : realItems;

    // 所有假期类型：从缓存获取完整列表（不只是有数据的）
    const cachedCodes = getAllLeaveCodesFromCache();
    const allLeaveTypes = cachedCodes
      .map((code) => ({
        leaveCode: code,
        leaveType: getLeaveTypeName(code) || code,
        hasQuota: realItems.some((item) => item.leaveCode === code),
      }))
      .filter((t) => t.leaveType && t.leaveCode);

    // 所有正常员工（含没有配额的）
    const allEmployees = dropdownEmployees.map((e) => ({
      userId: e.userId,
      name: e.name || e.userId,
      employeeNumber: e.employeeNumber || '',
    }));

    const employeeStatuses = await this.loadEmployeeStatuses(
      allEmployees.map((e) => e.userId),
    );

    return {
      lastSyncedAt: latestSnapshot?.snapshotAt?.toISOString() || null,
      summary: {
        employeeCount: visibleUserIds.size,
        leaveTypeCount: allLeaveTypes.length,
        nonZeroQuotaCount: realItems.filter(
          (item: AnnualLeaveQuotaItem) =>
            item.remainingDays > 0 || item.totalDays > 0,
        ).length,
      },
      allLeaveTypes,
      allEmployees,
      employeeStatuses,
      items,
    };
  }

  async getQuotaDetail(params: {
    userId: string;
    leaveCode: string;
  }): Promise<AnnualLeaveQuotaDetail | null> {
    const snapshotRepo = (this.prisma as any).dingtalkLeaveQuotaSnapshot;
    const row = (await snapshotRepo.findFirst({
      where: {
        userId: params.userId,
        leaveCode: params.leaveCode,
      },
      orderBy: [{ remainingDays: 'desc' }, { endDate: 'desc' }],
    })) as (LeaveQuotaSnapshotRow & { usageRecords?: any; releaseRecords?: any }) | null;

    if (!row) {
      return null;
    }

    // 从快照 JSON 字段读取缓存的记录（刷新快照时已写入）
    const usageRecords = Array.isArray(row.usageRecords) ? row.usageRecords : [];
    const releaseRecords = Array.isArray(row.releaseRecords) ? row.releaseRecords : [];

    const year = extractAnnualLeaveYear(row.leaveType);

    let localPlanTotalDays: number | null = null;
    let localPlanNotCountDays: number | null = null;
    let localPlanAdjustmentDays: number | null = null;
    let localPlanReleasedByToday: number | null = null;
    if (year) {
      try {
        const planRepo = (this.prisma as any).dingtalkAnnualLeaveReleasePlan;
        const plan = await planRepo.findUnique({
          where: { userId_year: { userId: row.userId, year } },
          select: { totalDays: true, notCountDays: true, adjustmentDays: true, releaseSchedule: true },
        });
        if (plan) {
          localPlanTotalDays = this.normalizeStoredNumber(plan.totalDays);
          localPlanNotCountDays = this.normalizeStoredNumber(plan.notCountDays);
          localPlanAdjustmentDays = this.normalizeStoredNumber(plan.adjustmentDays);
          const today = new Date();
          today.setHours(0, 0, 0, 0);
          const todayStr = this.formatDate(today);
          const schedule = Array.isArray(plan.releaseSchedule) ? plan.releaseSchedule : [];
          localPlanReleasedByToday = schedule.reduce((sum: number, entry: any) => {
            const date = String(entry?.releaseDate || '').trim();
            if (!date) return sum;
            return date <= todayStr ? sum + 1 : sum;
          }, 0);
        }
      } catch {
        /* 计划缺失不阻断 */
      }
    }

    // 员工在职段 + 停薪段 + 缓存司龄
    const [employmentPeriodsRaw, suspensionPeriodsRaw, employee] = await Promise.all([
      (this.prisma as any).dingtalkEmployeeEmploymentPeriod.findMany({
        where: { userId: row.userId },
        orderBy: { periodIndex: 'asc' },
      }),
      (this.prisma as any).dingtalkEmployeeSuspensionPeriod.findMany({
        where: { userId: row.userId },
        orderBy: { startDate: 'asc' },
      }),
      (this.prisma as any).dingtalkEmployee.findUnique({
        where: { userId: row.userId },
        select: { tenureDays: true, workStartDate: true },
      }),
    ]);

    const employmentPeriods = (employmentPeriodsRaw as any[]).map((p) => ({
      periodIndex: p.periodIndex,
      joinDate: this.formatDate(p.joinDate),
      leaveDate: p.leaveDate ? this.formatDate(p.leaveDate) : null,
      countInTenure: p.countInTenure,
      note: p.note || '',
    }));
    const suspensionPeriods = (suspensionPeriodsRaw as any[]).map((s) => ({
      startDate: this.formatDate(s.startDate),
      endDate: s.endDate ? this.formatDate(s.endDate) : null,
      reason: s.reason || '',
      note: s.note || '',
    }));

    return {
      userId: row.userId,
      name: row.employeeName,
      employeeNumber: row.employeeNumber,
      leaveType: row.leaveType,
      leaveCode: row.leaveCode,
      quotaCycle: row.quotaCycle,
      totalDays: this.normalizeStoredNumber(row.totalDays),
      usedDays: this.normalizeStoredNumber(row.usedDays),
      remainingDays: this.normalizeStoredNumber(row.remainingDays),
      startTime: row.startDate ? this.formatDate(row.startDate) : null,
      endTime: row.endDate ? this.formatDate(row.endDate) : null,
      usageRecords,
      releaseRecords,
      localPlanTotalDays,
      localPlanNotCountDays,
      localPlanAdjustmentDays,
      localPlanReleasedByToday,
      tenureDays: employee?.tenureDays ?? 0,
      workStartDate: employee?.workStartDate ? this.formatDate(employee.workStartDate) : null,
      employmentPeriods,
      suspensionPeriods,
    };
  }

  async getReleasePlan(params?: {
    year?: number;
    userId?: string;
    keyword?: string;
    upcomingOnly?: boolean;
  }): Promise<{
    summary: {
      employeeCount: number;
      planCount: number;
      upcomingDays: number;
      todayReleaseEmployeeCount: number;
    };
    items: AnnualLeaveReleasePlanItem[];
  }> {
    const year = String(params?.year || new Date().getFullYear());

    // 从本地员工表获取正常员工，只展示这些人
    const employeeRepo = (this.prisma as any).dingtalkEmployee;
    const activeEmployees = await employeeRepo.findMany({
      where: { status: '正常' },
      select: { userId: true, name: true, employeeNumber: true, joinDate: true, workStartDate: true },
    });
    const employeeMap = new Map<string, any>(activeEmployees.map((e: any) => [e.userId, e]));
    const activeUserIds = new Set(activeEmployees.map((e: any) => e.userId as string));

    const planRepo = (this.prisma as any).dingtalkAnnualLeaveReleasePlan;
    const records = (await planRepo.findMany({
      where: {
        year: Number(year),
      },
      orderBy: [{ userId: 'asc' }],
    })) as AnnualLeaveReleasePlanRow[];

    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const aggregated = new Map<string, AnnualLeaveReleasePlanItem>();
    const planStatusByUserId = new Map<
      string,
      { hasSchedule: boolean; totalDays: number }
    >();

    for (const record of records) {
      const userId = String(record.userId || '').trim();
      if (!userId) continue;
      if (params?.userId && userId !== params.userId) continue;
      // 只展示本地员工表中状态为正常的员工
      if (!activeUserIds.has(userId)) continue;

      const emp = employeeMap.get(userId);
      if (!emp) continue;
      const name = emp.name || userId;
      const employeeNumber = emp.employeeNumber || '';
      const status = '正常';
      const scheduleEntries = this.getReleaseScheduleEntries(record);
      planStatusByUserId.set(userId, {
        hasSchedule: scheduleEntries.length > 0,
        totalDays: Number(record.totalDays || 0),
      });

      for (const entry of scheduleEntries) {
        const date = entry.releaseDate;
        if (!date) continue;

        const releaseDate = this.parseDateOnly(date);
        releaseDate.setHours(0, 0, 0, 0);
        const released = releaseDate.getTime() <= today.getTime();
        if (params?.upcomingOnly && released) continue;

        const itemKey = `${userId}::${date}`;
        const existing = aggregated.get(itemKey);
        if (existing) {
          existing.days += 1;
          continue;
        }

        aggregated.set(itemKey, {
          date,
          userId,
          name,
          employeeNumber,
          status,
          days: 1,
          released,
          year,
          hasPlan: true,
          noPlanReason: null,
        });
      }
    }

    const items = Array.from(aggregated.values());

    // 为没有计划的正常员工添加无计划行
    for (const emp of activeEmployees) {
      const userId = emp.userId;
      if (params?.userId && userId !== params.userId) continue;
      const planState = planStatusByUserId.get(userId);
      if (planState?.hasSchedule) continue;

      if (
        !this.matchesKeyword(
          {
            userId,
            name: emp.name || userId,
            employeeNumber: emp.employeeNumber || '',
          },
          params?.keyword,
        )
      ) {
        continue;
      }

      const employee: EmployeeInfoRow = {
        userId,
        name: emp.name || userId,
        employeeNumber: emp.employeeNumber || '',
        status: '正常',
        joinDate: emp.joinDate ? this.formatDate(emp.joinDate) : '',
        workStartDate: emp.workStartDate ? this.formatDate(emp.workStartDate) : '',
      };

      items.push({
        date: '',
        userId,
        name: employee.name,
        employeeNumber: employee.employeeNumber,
        status: '正常',
        days: 0,
        released: false,
        year,
        hasPlan: false,
        noPlanReason: this.resolveNoPlanReason(employee, planState),
      });
    }

    const filteredItems = items
      .filter((item) => this.matchesKeyword(item, params?.keyword))
      .sort((a, b) => {
        if (a.date !== b.date) return a.date.localeCompare(b.date);
        if (a.name !== b.name) return a.name.localeCompare(b.name, 'zh-CN');
        return a.userId.localeCompare(b.userId);
      });

    return {
      summary: {
        employeeCount: new Set(filteredItems.map((item) => item.userId)).size,
        planCount: filteredItems.filter((item) => item.hasPlan).length,
        upcomingDays: filteredItems
          .filter((item) => item.hasPlan && !item.released)
          .reduce((sum, item) => sum + item.days, 0),
        todayReleaseEmployeeCount: new Set(
          filteredItems
            .filter(
              (item) => item.hasPlan && item.date === this.formatDate(today),
            )
            .map((item) => item.userId),
        ).size,
      },
      items: filteredItems,
    };
  }

  private async loadEmployees(userId?: string): Promise<EmployeeMeta[]> {
    // 优先从本地员工表读取，避免调钉钉 API
    const employeeRepo = (this.prisma as any).dingtalkEmployee;
    const where: Record<string, any> = {};
    if (userId) {
      where.userId = userId;
    } else {
      where.status = '正常';
    }

    const locals = await employeeRepo.findMany({
      where,
      select: { userId: true, name: true, employeeNumber: true },
    });

    if (locals.length > 0) {
      return locals.map((e: any) => ({
        userId: e.userId,
        name: e.name || e.userId,
        employeeNumber: e.employeeNumber || '',
      }));
    }

    // 本地表为空时回退到钉钉 API
    const ids = userId ? [userId] : await this.hrmService.getOnJobEmployeeIds();
    if (ids.length === 0) return [];

    const infoMap = await this.hrmService.getEmployeeInfoByIds(ids);
    return ids.map((id) => {
      const employee = infoMap[id];
      const fields = employee?.field_data_list || [];
      return {
        userId: id,
        name: this.getFieldValue(fields, '姓名') || id,
        employeeNumber: this.getFieldValue(fields, '工号') || '',
      };
    });
  }

  /**
   * 根据配额快照里的 (userId, leaveType) 抽取年份，批量加载本地年假释放计划。
   * 返回 map: `userId|year` → { totalDays, notCountDays, releasedByToday }
   */
  private async loadLocalAnnualLeavePlans(
    keys: Array<{ userId: string; leaveType: string }>,
  ): Promise<Map<string, { totalDays: number; notCountDays: number; releasedByToday: number }>> {
    const result = new Map<string, { totalDays: number; notCountDays: number; releasedByToday: number }>();
    const userIds = new Set<string>();
    const years = new Set<number>();
    for (const { userId, leaveType } of keys) {
      const year = extractAnnualLeaveYear(leaveType);
      if (year == null) continue;
      userIds.add(userId);
      years.add(year);
    }
    if (userIds.size === 0 || years.size === 0) return result;

    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const todayStr = this.formatDate(today);

    try {
      const planRepo = (this.prisma as any).dingtalkAnnualLeaveReleasePlan;
      const rows = await planRepo.findMany({
        where: {
          userId: { in: [...userIds] },
          year: { in: [...years] },
        },
        select: {
          userId: true,
          year: true,
          totalDays: true,
          notCountDays: true,
          releaseSchedule: true,
        },
      });
      for (const row of rows) {
        const schedule = Array.isArray(row.releaseSchedule) ? row.releaseSchedule : [];
        const releasedByToday = schedule.reduce((sum: number, entry: any) => {
          const date = String(entry?.releaseDate || '').trim();
          if (!date) return sum;
          return date <= todayStr ? sum + 1 : sum;
        }, 0);
        result.set(`${row.userId}|${row.year}`, {
          totalDays: this.normalizeStoredNumber(row.totalDays),
          notCountDays: this.normalizeStoredNumber(row.notCountDays),
          releasedByToday,
        });
      }
    } catch {
      // 计划缺失不阻断主流程
    }
    return result;
  }

  /**
   * 批量加载员工状态：employment_periods 段数、是否停薪中、是否有停薪记录。
   */
  private async loadEmployeeStatuses(
    userIds: string[],
  ): Promise<EmployeeStatusInfo[]> {
    if (userIds.length === 0) return [];
    try {
      const periodRepo = (this.prisma as any).dingtalkEmployeeEmploymentPeriod;
      const suspensionRepo = (this.prisma as any).dingtalkEmployeeSuspensionPeriod;
      const [periodRows, suspensionRows] = await Promise.all([
        periodRepo.findMany({
          where: { userId: { in: userIds } },
          select: { userId: true },
        }),
        suspensionRepo.findMany({
          where: { userId: { in: userIds } },
          select: { userId: true, startDate: true, endDate: true },
        }),
      ]);
      const periodCountMap = new Map<string, number>();
      for (const r of periodRows) {
        periodCountMap.set(r.userId, (periodCountMap.get(r.userId) ?? 0) + 1);
      }
      const suspensionByUser = new Map<string, Array<{ startDate: Date; endDate: Date | null }>>();
      for (const r of suspensionRows) {
        const arr = suspensionByUser.get(r.userId) ?? [];
        arr.push({ startDate: r.startDate, endDate: r.endDate });
        suspensionByUser.set(r.userId, arr);
      }
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      return userIds.map((userId) => {
        const suspensions = suspensionByUser.get(userId) ?? [];
        const hasActiveSuspension = suspensions.some((s) => {
          const started = s.startDate <= today;
          const notEnded = !s.endDate || s.endDate >= today;
          return started && notEnded;
        });
        return {
          userId,
          periodCount: periodCountMap.get(userId) ?? 0,
          hasAnySuspension: suspensions.length > 0,
          hasActiveSuspension,
        };
      });
    } catch {
      return userIds.map((userId) => ({
        userId,
        periodCount: 0,
        hasAnySuspension: false,
        hasActiveSuspension: false,
      }));
    }
  }

  private async loadReleaseRecords(
    userId: string,
    leaveCode: string,
  ): Promise<AnnualLeaveQuotaReleaseRecord[]> {
    const leaveType = getLeaveTypeName(leaveCode) || '';
    const year = extractAnnualLeaveYear(leaveType);
    if (year == null) {
      return [];
    }
    const planRepo = (this.prisma as any).dingtalkAnnualLeaveReleasePlan;
    const records = (await planRepo.findMany({
      where: {
        year,
        userId,
      },
      orderBy: {
        updatedAt: 'desc',
      },
      take: 1,
    })) as AnnualLeaveReleasePlanRow[];
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const releaseMap = new Map<string, AnnualLeaveQuotaReleaseRecord>();
    for (const record of records) {
      for (const entry of this.getReleaseScheduleEntries(record)) {
        const date = entry.releaseDate;
        if (!date) continue;

        const existing = releaseMap.get(date);
        if (existing) {
          existing.days += 1;
          continue;
        }

        const releaseDate = this.parseDateOnly(date);
        releaseDate.setHours(0, 0, 0, 0);
        releaseMap.set(date, {
          date,
          days: 1,
          released: releaseDate.getTime() <= today.getTime(),
          year: String(year),
        });
      }
    }

    return Array.from(releaseMap.values()).sort((a, b) =>
      a.date.localeCompare(b.date),
    );
  }

  private getReleaseScheduleEntries(
    record: AnnualLeaveReleasePlanRow,
  ): Array<{ dayIndex: number; releaseDate: string }> {
    if (!Array.isArray(record.releaseSchedule)) {
      return [];
    }

    return record.releaseSchedule
      .map((item: any) => ({
        dayIndex: Number(item?.dayIndex || 0),
        releaseDate: String(item?.releaseDate || '').trim(),
      }))
      .filter((item) => item.dayIndex > 0 && !!item.releaseDate)
      .sort((a, b) => a.dayIndex - b.dayIndex);
  }

  private resolveNoPlanReason(
    employee: EmployeeInfoRow,
    planState?: { hasSchedule: boolean; totalDays: number },
  ): string {
    if (!employee.joinDate && !employee.workStartDate) {
      return '缺少入职日期和首次参加工作日期';
    }
    if (!employee.joinDate) {
      return '缺少入职日期';
    }
    if (!employee.workStartDate) {
      return '缺少首次参加工作日期';
    }
    if (planState && planState.totalDays === 0) {
      return '本年度无可释放年假';
    }
    if (planState && !planState.hasSchedule) {
      return '本年度计划存在，但未生成释放日期';
    }
    return '尚未生成本年度释放计划，请先更新中间表';
  }

  private getFieldValue(fieldDataList: any[], fieldName: string): string {
    const field = fieldDataList.find(
      (item: any) => item.field_name === fieldName,
    );
    return String(field?.field_value_list?.[0]?.value || '').trim();
  }

  private normalizeQuotaNumber(value: unknown): number {
    const num = Number(value || 0);
    if (Number.isNaN(num)) return 0;
    return num / 100;
  }

  private normalizeStoredNumber(value: unknown): number {
    const num = Number(value || 0);
    return Number.isNaN(num) ? 0 : num;
  }

  private mapUsageRecord(record: any): AnnualLeaveQuotaUsageRecord {
    // 兼容新版（驼峰）和旧版（下划线）字段名
    const startTimeRaw = record.startTime || record.start_time || null;
    const endTimeRaw = record.endTime || record.end_time || null;
    const daysRaw = record.recordNumPerDay ?? record.record_num_per_day ?? 0;
    const days = this.normalizeQuotaNumber(daysRaw);
    const startTime = this.formatUnknownDateTime(startTimeRaw);
    const endTime = this.formatUnknownDateTime(endTimeRaw);
    const gmtCreate = record.gmtCreate || record.gmt_create;
    const operationDate = gmtCreate ? this.formatUnknownDateTime(gmtCreate) : null;
    const status = record.leaveStatus || record.leave_status || '';

    return {
      date: startTime ? startTime.slice(0, 10) : null,
      startTime,
      endTime,
      days,
      status: String(status),
      recordType: String(record.leaveRecordType || record.leave_record_type || ''),
      operationDate: operationDate?.slice(0, 10) || null,
    };
  }

  private parseTimestampToDate(value: unknown): Date | null {
    const timestamp = Number(value || 0);
    if (!timestamp) return null;
    const date = new Date(timestamp);
    return new Date(date.getFullYear(), date.getMonth(), date.getDate());
  }

  private formatDate(date: Date): string {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  private parseDateOnly(value: string): Date {
    const [year, month, day] = value.split('-').map((part) => Number(part));
    return new Date(year, (month || 1) - 1, day || 1);
  }

  private formatUnknownDateTime(value: unknown): string | null {
    if (!value) return null;
    const timestamp = Number(value);
    const date = Number.isNaN(timestamp)
      ? new Date(String(value))
      : new Date(timestamp);
    if (Number.isNaN(date.getTime())) return String(value);

    const parts = new Intl.DateTimeFormat('zh-CN', {
      timeZone: 'Asia/Shanghai',
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false,
    }).formatToParts(date);

    const partMap = new Map(parts.map((part) => [part.type, part.value]));
    return `${partMap.get('year')}-${partMap.get('month')}-${partMap.get('day')} ${partMap.get('hour')}:${partMap.get('minute')}`;
  }

  private matchesKeyword(
    item: { name?: string; employeeNumber?: string; userId?: string },
    keyword?: string,
  ): boolean {
    const normalized = (keyword || '').trim();
    if (!normalized) return true;
    return [item.name, item.employeeNumber, item.userId]
      .filter(Boolean)
      .some((value) => String(value).includes(normalized));
  }
}
