import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { DingtalkYidaService } from '../sdk/dingtalk-yida.service';
import { DingtalkAttendanceService } from '../sdk/dingtalk-attendance.service';
import {
  ANNUAL_LEAVE_RELEASE_CODE,
  getAllLeaveCodesFromCache,
  getLeaveTypeName,
} from '../constants';
import { SyncExecutionResult } from './business-trip-sync.service';
import { SyncLogger } from './sync-logger';
type SuspensionRow = { startDate: Date; endDate: Date | null };

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

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

  constructor(
    private prisma: PrismaService,
    private yidaService: DingtalkYidaService,
    private attendanceService: DingtalkAttendanceService,
  ) {}

  /**
   * 年假释放：读取中间表 + 发放到钉钉
   * 只负责"发放"，不做计算。计算由 refreshPlan 完成。
   */
  async sync(userId?: string, year?: number): Promise<SyncExecutionResult> {
    const logger = new SyncLogger('AnnualLeaveSyncService');
    const startTime = Date.now();
    let count = 0;
    const errors: string[] = [];

    logger.log('----年假释放开始----');
    if (userId) logger.log(`指定员工: ${userId}`);

    try {
      const today = new Date();
      const currentYear = year ?? today.getFullYear();
      const currentYearStr = String(currentYear);
      const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());

      // 1. 从中间表读取当年释放计划
      const planRepo = (this.prisma as any).dingtalkAnnualLeaveReleasePlan;
      const plans = (await planRepo.findMany({
        where: {
          year: currentYear,
          ...(userId ? { userId } : {}),
        },
      })) as AnnualLeaveReleasePlanRow[];
      logger.log(`当年释放计划: ${plans.length} 条`);

      // 2. 从本地员工表获取正常员工（只给正常员工发放）
      const employeeRepo = (this.prisma as any).dingtalkEmployee;
      const activeEmployees = await employeeRepo.findMany({
        where: { status: '正常' },
        select: { userId: true, name: true },
      });
      const activeUserIds = new Set(activeEmployees.map((e: any) => e.userId as string));
      const userNameMap = new Map(activeEmployees.map((e: any) => [e.userId as string, (e.name || e.userId) as string]));
      logger.log(`正常员工: ${activeUserIds.size} 人`);

      // 3. 动态获取当年的年假 leave_code
      const leaveCode = this.getLeaveCodeForYear(currentYear);
      if (!leaveCode) {
        throw new Error(`找不到 ${currentYear} 年年假对应的 leave_code`);
      }
      logger.log(`${currentYear}年年假 leave_code: ${leaveCode}`);

      // 4. 构建发放列表
      const startTimestamp = new Date(currentYear, 0, 1).getTime();
      const endTimestamp = new Date(currentYear + 1, 2, 31, 23, 59, 59).getTime();
      const leaveQuotas: any[] = [];

      for (const plan of plans) {
        const uid = String(plan.userId || '').trim();
        if (!uid) continue;
        if (!activeUserIds.has(uid)) continue;

        const adjustData = Number(plan.adjustmentDays || 0);
        const dateObjs = this.getReleaseDatesFromPlan(plan);

        // 截止今天已到期的天数（今天 >= 释放日即算到期）
        const releasedCount = dateObjs.filter(d => d <= todayDate).length;
        if (releasedCount === 0) continue;

        const quota = Math.max(0, (releasedCount - adjustData) * 100);

        // quota_num_per_day 是目标总额，重复调用不会多发，所以无需判断"今天是否是释放日"
        leaveQuotas.push({
          userid: uid,
          start_time: startTimestamp,
          end_time: endTimestamp,
          reason: '系统自动释放年假',
          quota_num_per_day: Math.floor(quota),
          quota_cycle: currentYearStr,
          leave_code: leaveCode,
        });
      }

      // 跳过的员工统计
      const skippedInactive = plans.filter(p => {
        const uid = String(p.userId || '').trim();
        return uid && !activeUserIds.has(uid);
      }).length;
      const skippedNoRelease = plans.length - skippedInactive - leaveQuotas.length
        - plans.filter(p => !String(p.userId || '').trim()).length;

      logger.log(`今日需释放: ${leaveQuotas.length} 人，跳过非正常员工: ${skippedInactive} 人，今日无需释放: ${skippedNoRelease} 人`);

      // 查询当前钉钉上的额度，用于日志对比
      const quotaUserIds = leaveQuotas.map((q: any) => q.userid);
      const currentQuotas = quotaUserIds.length > 0
        ? await this.attendanceService.searchLeaveQuota(leaveCode, quotaUserIds)
        : [];
      const currentQuotaMap = new Map<string, number>();
      for (const cq of currentQuotas) {
        if (cq.quota_cycle === String(currentYear)) {
          currentQuotaMap.set(cq.userid, Number(cq.quota_num_per_day || 0));
        }
      }

      // 过滤掉额度未变化的员工
      const changedQuotas: any[] = [];
      let unchangedCount = 0;
      for (const q of leaveQuotas) {
        const oldQuota = currentQuotaMap.get(q.userid) || 0;
        if (oldQuota === q.quota_num_per_day) {
          unchangedCount++;
        } else {
          changedQuotas.push({ ...q, _oldQuota: oldQuota });
        }
      }

      logger.log(`需发放: ${changedQuotas.length} 人，额度无变化跳过: ${unchangedCount} 人`);
      for (const q of changedQuotas) {
        const name = userNameMap.get(q.userid) || q.userid;
        logger.log(`  ${name}：${(q._oldQuota / 100).toFixed(1)} → ${(q.quota_num_per_day / 100).toFixed(1)} 天`);
      }

      // 5. 批量发放到钉钉（仅发放有变化的）
      for (let i = 0; i < changedQuotas.length; i += 50) {
        const batch = changedQuotas.slice(i, i + 50);
        logger.log(`发放第 ${Math.floor(i / 50) + 1} 批（${batch.length} 人）到钉钉...`);
        await this.attendanceService.updateLeaveQuota(batch);
        count += batch.length;
      }
    } catch (error: any) {
      errors.push(`年假释放失败: ${error.message}`);
      logger.error(`年假释放失败: ${error.message}`);
    }

    logger.log(`----年假释放完成，共处理${count}条----`);

    return {
      success: errors.length === 0,
      processedCount: count,
      errors,
      duration: Date.now() - startTime,
      logs: logger.getText(),
    };
  }

  /**
   * 更新中间表：从本地员工表读取数据，计算释放日期，写入中间表。
   * 不发放到钉钉。
   */
  async refreshPlan(userId?: string, year?: number): Promise<SyncExecutionResult> {
    const logger = new SyncLogger('AnnualLeaveSyncService');
    const startTime = Date.now();
    let count = 0;
    const errors: string[] = [];

    logger.log('----年假计划重算开始----');
    if (userId) logger.log(`指定员工: ${userId}`);

    try {
      const currentYear = year ?? new Date().getFullYear();
      const planRepo = (this.prisma as any).dingtalkAnnualLeaveReleasePlan;

      // 1. 从本地员工表读取正常员工
      const employeeRepo = (this.prisma as any).dingtalkEmployee;
      const where: Record<string, any> = { status: '正常' };
      if (userId) where.userId = userId;
      const employees = await employeeRepo.findMany({
        where,
        select: { userId: true, name: true, employeeNumber: true, joinDate: true, workStartDate: true },
      });
      logger.log(`正常员工: ${employees.length} 人`);

      // 2. 获取当年已有计划（用于保留 adjustmentDays / notCountDays）
      const existingPlans = (await planRepo.findMany({
        where: {
          year: currentYear,
          ...(userId ? { userId } : {}),
        },
      })) as AnnualLeaveReleasePlanRow[];
      const existingPlanMap = new Map<string, AnnualLeaveReleasePlanRow>();
      for (const plan of existingPlans) {
        existingPlanMap.set(plan.userId, plan);
      }

      // 3. 获取去年计划（用于计算 bias）
      const lastYearPlans = (await planRepo.findMany({
        where: {
          year: currentYear - 1,
          ...(userId ? { userId } : {}),
        },
      })) as AnnualLeaveReleasePlanRow[];
      const lastYearDict: Record<string, AnnualLeaveReleasePlanRow> = {};
      for (const plan of lastYearPlans) {
        lastYearDict[plan.userId] = plan;
      }

      // 4. 逐个员工计算（以目标年 1 月 1 日为司龄基准；支持一年多段在职窗口）
      for (const emp of employees) {
        const uid = emp.userId;
        const name = emp.name || uid;
        const workStartDateStr = emp.workStartDate ? this.formatDate(emp.workStartDate) : '';
        const existing = existingPlanMap.get(uid);

        const writeEmptyPlan = async (reason: string) => {
          logger.log(`员工${name}${reason}，写入空计划`);
          const planData = this.buildAnnualLeavePlanData(uid, [], currentYear, {
            adjustmentDays: Number(existing?.adjustmentDays || 0),
            notCountDays: Number(existing?.notCountDays || 0),
            employeeName: name,
          });
          await planRepo.upsert({
            where: { userId_year: { userId: uid, year: currentYear } },
            update: planData,
            create: planData,
          });
          count++;
        };

        const suspensions: SuspensionRow[] = await (this.prisma as any).dingtalkEmployeeSuspensionPeriod.findMany({
          where: { userId: uid },
          orderBy: { startDate: 'asc' },
          select: { startDate: true, endDate: true },
        });

        const rawWindows = await this.getEmploymentWindowsInYear(uid, emp.joinDate, currentYear);

        if (rawWindows.length === 0) {
          await writeEmptyPlan(`${currentYear}年未在职`);
          continue;
        }

        if (!workStartDateStr) {
          await writeEmptyPlan('缺少首次工作日期');
          continue;
        }

        // 按停薪留职时段切割窗口（停薪期间不释放年假）
        const windows = this.splitWindowsBySuspensions(rawWindows, currentYear, suspensions);
        if (windows.length === 0) {
          await writeEmptyPlan(`${currentYear}年全年停薪留职`);
          continue;
        }

        const merged = new Set<string>();
        const workStartDate = emp.workStartDate as Date;

        for (const win of windows) {
          const planJoinDateStr = this.formatDate(win.planJoinDate);
          const windowStartStr = this.formatDate(win.windowStart);
          const windowEndStr = this.formatDate(win.windowEnd);

          // 跨年 carryover 只作用于第一个窗口（Jan 1 在职段）
          let bias = 0;
          if (win.isFirstWindow && win.windowStart.getTime() === new Date(currentYear, 0, 1).getTime()) {
            bias = Number(existing?.notCountDays || 0);
            bias += await this.ensureLastYearPlan(
              uid, name, planJoinDateStr, workStartDateStr, currentYear, lastYearDict, logger,
            );
          }

          const hasAccumulation = win.planJoinDate.getTime() < win.windowStart.getTime();
          let winDates: string[];

          if (hasAccumulation) {
            // 累计司龄：按累计档次固定天数，在窗口内部均分
            const workYears = currentYear - workStartDate.getFullYear();
            const companyYears = currentYear - win.planJoinDate.getFullYear();
            const leaveDays = this.calculateLeaveDays(
              Math.max(0, workYears - 1),
              Math.max(0, companyYears - 1),
            );
            if (leaveDays > 0) {
              const winStart = new Date(win.windowStart.getTime() + bias * 86400000);
              const winEndExclusive = new Date(win.windowEnd.getTime() + 86400000);
              const yearEndDate = new Date(currentYear, 11, 31);
              winDates = this.distributeLeaveDays(leaveDays, winStart, winEndExclusive)
                .map((d) => (d.getFullYear() > currentYear ? yearEndDate : d))
                .filter((d) => d.getFullYear() === currentYear)
                .map((d) => this.formatDate(d));
            } else {
              winDates = [];
            }
            logger.log(
              `员工${name} 累计窗口 [${windowStartStr}, ${windowEndStr}]：年档 ${leaveDays} 天 → 窗口内释放 ${winDates.length} 条`,
            );
          } else {
            // 纯首年新员工：走 calculateLeaveDates 的首年分支（保留工龄切档）+ 裁剪
            const rawDates = this.calculateLeaveDates(planJoinDateStr, workStartDateStr, bias, currentYear);
            winDates = rawDates.filter((d) => d >= windowStartStr && d <= windowEndStr);
            const dropped = rawDates.length - winDates.length;
            if (dropped > 0) {
              logger.log(
                `员工${name} 首年窗口 [${windowStartStr}, ${windowEndStr}]：裁剪 ${dropped} 条（原 ${rawDates.length} → ${winDates.length}）`,
              );
            }
          }

          for (const d of winDates) merged.add(d);
        }

        const leaveDates = [...merged].sort();

        const planData = this.buildAnnualLeavePlanData(uid, leaveDates, currentYear, {
          adjustmentDays: Number(existing?.adjustmentDays || 0),
          notCountDays: Number(existing?.notCountDays || 0),
          employeeName: name,
        });

        await planRepo.upsert({
          where: { userId_year: { userId: uid, year: currentYear } },
          update: planData,
          create: planData,
        });
        count++;
      }
    } catch (error: any) {
      errors.push(`年假计划重算失败: ${error.message}`);
      logger.error(`年假计划重算失败: ${error.message}`);
    }

    logger.log(`----年假计划重算完成，共处理${count}条----`);

    return {
      success: errors.length === 0,
      processedCount: count,
      errors,
      duration: Date.now() - startTime,
      logs: logger.getText(),
    };
  }

  /**
   * 根据年份动态获取对应的年假 leave_code
   * 从缓存中匹配 "XXXX年年假" 或 "XXXX 年年假"
   */
  private getLeaveCodeForYear(year: number): string | null {
    const allCodes = getAllLeaveCodesFromCache();
    for (const code of allCodes) {
      const name = getLeaveTypeName(code);
      if (name && name.includes(`${year}`) && name.includes('年假')) {
        return code;
      }
    }
    // 回退到硬编码（仅 2025）
    if (year === 2025) return ANNUAL_LEAVE_RELEASE_CODE;
    return null;
  }

  /**
   * 计算目标年的释放计划入职基准
   *
   * - accumulated：目标年 1 月 1 日当天在职 → 用截至 Jan 1 的累计司龄反推虚拟入职日
   * - new_period：年初不在职但当年有入职/再入职段 → 用该段的真实入职日（走首年规则）
   * - inactive：当年从未在职 → 跳过
   *
   * 所有累计都以 targetYear 的 1 月 1 日为基准，不取 "today"，保证历史年度可重算。
   */
  /**
   * 按停薪留职时段切割在职窗口。停薪留职期间不释放年假（HR 口径）；
   * 但停薪**计入司龄**，所以 planJoinDate 反推累计时不扣停薪交集。
   * 一个窗口被中间停薪切开 → 变成前后两段子窗口，各自独立走累计/首年分布。
   */
  private splitWindowsBySuspensions<T extends { planJoinDate: Date; windowStart: Date; windowEnd: Date; isFirstWindow: boolean; originalJoinDate: Date }>(
    windows: T[],
    targetYear: number,
    suspensions: SuspensionRow[],
  ): T[] {
    if (suspensions.length === 0) return windows;

    const yearEnd = new Date(targetYear, 11, 31);

    // 裁剪到目标年内的停薪区间
    const relevant = suspensions
      .map((s) => ({
        start: s.startDate,
        end: s.endDate ?? yearEnd,
      }))
      .filter((s) => s.end >= new Date(targetYear, 0, 1) && s.start <= yearEnd)
      .sort((a, b) => a.start.getTime() - b.start.getTime());

    if (relevant.length === 0) return windows;

    const result: T[] = [];
    for (const win of windows) {
      let segments: Array<{ start: Date; end: Date }> = [{ start: win.windowStart, end: win.windowEnd }];
      for (const s of relevant) {
        const next: Array<{ start: Date; end: Date }> = [];
        for (const seg of segments) {
          if (s.end < seg.start || s.start > seg.end) {
            next.push(seg);
            continue;
          }
          if (s.start > seg.start) {
            next.push({ start: seg.start, end: new Date(Math.min(seg.end.getTime(), s.start.getTime() - 86400000)) });
          }
          if (s.end < seg.end) {
            next.push({ start: new Date(Math.max(seg.start.getTime(), s.end.getTime() + 86400000)), end: seg.end });
          }
        }
        segments = next.filter((seg) => seg.start <= seg.end);
      }
      for (const seg of segments) {
        result.push({
          ...win,
          windowStart: seg.start,
          windowEnd: seg.end,
          // isFirstWindow 只保留在最早的子窗口（保持 bias carryover 只作用一次）
          isFirstWindow: win.isFirstWindow && seg.start.getTime() === win.windowStart.getTime(),
        });
      }
    }
    return result;
  }

  /**
   * 目标年的所有在职窗口（含 Jan 1 在职段 + 当年再入职段）
   *
   * 每个窗口的 `planJoinDate` 都是 "窗口起始日 - 该日之前的累计司龄天数"，
   * 使 calculateLeaveDates 能按累计司龄档次给出对应天数，再裁剪到窗口内。
   *
   * `isFirstWindow=true` 的窗口（即 Jan 1 在职段）会继承上年 bias / ensureLastYearPlan。
   */
  private async getEmploymentWindowsInYear(
    userId: string,
    originalJoinDate: Date | null,
    targetYear: number,
  ): Promise<Array<{
    planJoinDate: Date;
    windowStart: Date;
    windowEnd: Date;
    isFirstWindow: boolean;
    originalJoinDate: Date;
  }>> {
    const yearStart = new Date(targetYear, 0, 1);
    const yearEnd = new Date(targetYear, 11, 31);
    const clampToYearEnd = (d: Date | null): Date => (!d || d > yearEnd ? yearEnd : d);

    const repo = (this.prisma as any).dingtalkEmployeeEmploymentPeriod;
    const periods: Array<{
      joinDate: Date;
      leaveDate: Date | null;
      countInTenure: boolean;
      periodIndex: number;
    }> = await repo.findMany({
      where: { userId },
      orderBy: { periodIndex: 'asc' },
    });

    // 截至某日的累计 countInTenure 天数（停薪留职计入司龄，不扣减）
    const accumulatedDaysAsOf = (
      list: typeof periods,
      cutoff: Date,
    ): number =>
      list
        .filter((p) => p.countInTenure)
        .reduce((sum, p) => {
          if (p.joinDate >= cutoff) return sum;
          const end = p.leaveDate ?? cutoff;
          const effectiveEnd = end < cutoff ? end : cutoff;
          return sum + Math.max(0, Math.floor((effectiveEnd.getTime() - p.joinDate.getTime()) / 86400000));
        }, 0);

    // 无 employment_periods → 用 DingtalkEmployee.joinDate 兜底构造单窗口
    if (periods.length === 0) {
      if (!originalJoinDate || originalJoinDate > yearEnd) return [];
      if (originalJoinDate <= yearStart) {
        return [{
          planJoinDate: originalJoinDate,
          windowStart: yearStart,
          windowEnd: yearEnd,
          isFirstWindow: true,
          originalJoinDate,
        }];
      }
      return [{
        planJoinDate: originalJoinDate,
        windowStart: originalJoinDate,
        windowEnd: yearEnd,
        isFirstWindow: true,
        originalJoinDate,
      }];
    }

    const windows: Array<{
      planJoinDate: Date;
      windowStart: Date;
      windowEnd: Date;
      isFirstWindow: boolean;
      originalJoinDate: Date;
    }> = [];

    // 1) Jan 1 在职段
    const activeOnJan1 = periods.find((p) => {
      const pEnd = p.leaveDate ?? yearEnd;
      return p.joinDate <= yearStart && pEnd >= yearStart;
    });
    if (activeOnJan1) {
      const accumulatedDays = accumulatedDaysAsOf(periods, yearStart);
      windows.push({
        planJoinDate: new Date(yearStart.getTime() - accumulatedDays * 86400000),
        windowStart: yearStart,
        windowEnd: clampToYearEnd(activeOnJan1.leaveDate),
        isFirstWindow: true,
        originalJoinDate: activeOnJan1.joinDate,
      });
    }

    // 2) 当年起始的段（年初新入职 / 年内再入职）
    for (const p of periods) {
      if (p.joinDate < yearStart || p.joinDate > yearEnd) continue;
      // Jan 1 当天就在职的段已由上面处理，跳过
      if (activeOnJan1 && p === activeOnJan1) continue;
      // 窗口起始日之前的累计司龄（包含本段之前的所有 countInTenure 段）
      const accumulatedDays = accumulatedDaysAsOf(periods, p.joinDate);
      windows.push({
        planJoinDate: new Date(p.joinDate.getTime() - accumulatedDays * 86400000),
        windowStart: p.joinDate,
        windowEnd: clampToYearEnd(p.leaveDate),
        isFirstWindow: windows.length === 0,
        originalJoinDate: p.joinDate,
      });
    }

    windows.sort((a, b) => a.windowStart.getTime() - b.windowStart.getTime());
    return windows;
  }

  // ========== 以下是计算相关的辅助方法（保持不变）==========

  /**
   * 计算年假释放日期
   */
  calculateLeaveDates(
    joinDateStr: string,
    workStartDateStr: string,
    bias: number,
    targetYear: number,
  ): string[] {
    const joinDate = this.parseDateOnly(joinDateStr);
    const workStartDate = this.parseDateOnly(workStartDateStr);
    const currentYear = targetYear;

    // 入职时间在目标年之后，该年没有年假
    if (joinDate.getFullYear() > currentYear) return [];

    const joinAnniversary = new Date(currentYear, joinDate.getMonth(), joinDate.getDate());
    const workAnniversary = new Date(currentYear, workStartDate.getMonth(), workStartDate.getDate());

    const companyYears = currentYear - joinDate.getFullYear();
    const workYears = currentYear - workStartDate.getFullYear();

    let leaveDates: Date[] = [];

    if (currentYear === joinDate.getFullYear()) {
      // 第一年
      const firstStart = new Date(joinDate.getTime() + bias * 86400000);
      const firstEnd = new Date(currentYear + 1, 0, 1);

      if (workAnniversary > joinAnniversary) {
        const firstLeaveDays = this.calculateLeaveDays(workYears - 1, companyYears);
        const secondLeaveDays = this.calculateLeaveDays(workYears, companyYears);
        const firstDates = this.distributeLeaveDays(firstLeaveDays, firstStart, workAnniversary);

        if (secondLeaveDays === 0) {
          leaveDates = firstDates;
        } else {
          let secondStart: Date;
          if (firstDates.length === 0) {
            const legacyDays = Math.floor(firstLeaveDays / secondLeaveDays * this.daysBetween(firstStart, workAnniversary));
            secondStart = new Date(workAnniversary.getTime() - legacyDays * 86400000);
          } else if (firstDates[firstDates.length - 1] < workAnniversary) {
            const legacyDays = Math.floor(firstLeaveDays / secondLeaveDays * this.daysBetween(firstDates[firstDates.length - 1], workAnniversary));
            secondStart = new Date(workAnniversary.getTime() - legacyDays * 86400000);
          } else {
            secondStart = workAnniversary;
          }
          const secondDates = this.distributeLeaveDays(secondLeaveDays, secondStart, firstEnd);
          leaveDates = [...firstDates, ...secondDates];
        }
      } else {
        const leaveDays = this.calculateLeaveDays(workYears, companyYears);
        leaveDates = this.distributeLeaveDays(leaveDays, firstStart, firstEnd);
      }
    } else if (currentYear === joinDate.getFullYear() + 1) {
      // 第二年
      const firstStart = new Date(new Date(currentYear, 0, 1).getTime() + bias * 86400000);

      if (workAnniversary < joinAnniversary) {
        const firstLeaveDays = this.calculateLeaveDays(workYears - 1, companyYears - 1);
        const secondLeaveDays = this.calculateLeaveDays(workYears, companyYears - 1);
        const thirdLeaveDays = this.calculateLeaveDays(workYears, companyYears);

        const firstDates = this.distributeLeaveDays(firstLeaveDays, firstStart, workAnniversary);
        leaveDates = [...firstDates];

        if (secondLeaveDays !== 0) {
          let secondStart: Date;
          if (firstDates.length === 0) {
            const legacyDays = Math.floor(firstLeaveDays / secondLeaveDays * this.daysBetween(firstStart, workAnniversary));
            secondStart = new Date(workAnniversary.getTime() - legacyDays * 86400000);
          } else if (firstDates[firstDates.length - 1] < workAnniversary) {
            const legacyDays = Math.floor(firstLeaveDays / secondLeaveDays * this.daysBetween(firstDates[firstDates.length - 1], workAnniversary));
            secondStart = new Date(workAnniversary.getTime() - legacyDays * 86400000);
          } else {
            secondStart = workAnniversary;
          }
          const secondEnd = joinAnniversary;
          const secondDates = this.distributeLeaveDays(secondLeaveDays, secondStart, secondEnd);
          leaveDates.push(...secondDates);

          if (thirdLeaveDays !== 0) {
            let thirdStart: Date;
            if (secondDates.length === 0) {
              const legacyDays = Math.floor(secondLeaveDays / thirdLeaveDays * this.daysBetween(secondStart, secondEnd));
              thirdStart = new Date(secondEnd.getTime() - legacyDays * 86400000);
            } else if (secondDates[secondDates.length - 1] < new Date(joinAnniversary.getTime() - 86400000)) {
              const legacyDays = Math.floor(secondLeaveDays / thirdLeaveDays * this.daysBetween(secondDates[secondDates.length - 1], secondEnd));
              thirdStart = new Date(secondEnd.getTime() - legacyDays * 86400000);
            } else {
              thirdStart = secondEnd;
            }
            const thirdEnd = new Date(currentYear + 1, 0, 1);
            const thirdDates = this.distributeLeaveDays(thirdLeaveDays, thirdStart, thirdEnd);
            leaveDates.push(...thirdDates);
          }
        }
      } else {
        const firstLeaveDays = this.calculateLeaveDays(workYears - 1, companyYears - 1);
        const secondLeaveDays = this.calculateLeaveDays(workYears, companyYears);
        const firstEnd = joinAnniversary;
        const firstDates = this.distributeLeaveDays(firstLeaveDays, firstStart, firstEnd);
        leaveDates = [...firstDates];

        if (secondLeaveDays !== 0) {
          let secondStart: Date;
          if (firstDates.length === 0) {
            const legacyDays = Math.floor(firstLeaveDays / secondLeaveDays * this.daysBetween(firstStart, firstEnd));
            secondStart = new Date(firstEnd.getTime() - legacyDays * 86400000);
          } else if (firstDates[firstDates.length - 1] < firstEnd) {
            const legacyDays = Math.floor(firstLeaveDays / secondLeaveDays * this.daysBetween(firstDates[firstDates.length - 1], firstEnd));
            secondStart = new Date(firstEnd.getTime() - legacyDays * 86400000);
          } else {
            secondStart = firstEnd;
          }
          const secondEnd = new Date(currentYear + 1, 0, 1);
          const secondDates = this.distributeLeaveDays(secondLeaveDays, secondStart, secondEnd);
          leaveDates.push(...secondDates);
        }
      }
    } else {
      // 第三年及以后
      const leaveDays = this.calculateLeaveDays(workYears - 1, companyYears - 1);
      const startDate = new Date(new Date(currentYear, 0, 1).getTime() + bias * 86400000);
      const endDate = new Date(currentYear + 1, 0, 1);
      leaveDates = this.distributeLeaveDays(leaveDays, startDate, endDate);
    }

    // 最后一天可能因为间隔计算落在下一年 1月1日，应归入当年 12月31日
    const yearEnd = new Date(currentYear, 11, 31);
    return leaveDates
      .map(d => d.getFullYear() > currentYear ? yearEnd : d)
      .filter(d => d.getFullYear() === currentYear)
      .map(d => this.formatDate(d));
  }

  calculateLeaveDays(workYears: number, companyYears: number): number {
    let workLeaveDays: number;
    if (workYears < 1) workLeaveDays = 0;
    else if (workYears < 10) workLeaveDays = 5;
    else if (workYears < 20) workLeaveDays = 10;
    else workLeaveDays = 15;

    let companyLeaveDays: number;
    if (companyYears < 1) companyLeaveDays = workLeaveDays;
    else if (companyYears < 5) companyLeaveDays = 15;
    else if (companyYears <= 9) companyLeaveDays = companyYears + 11;
    else companyLeaveDays = 20;

    return companyLeaveDays;
  }

  distributeLeaveDays(leaveDays: number, startDate: Date, endDate: Date): Date[] {
    const totalDays = this.daysBetween(startDate, endDate);
    const intervals = Math.floor(totalDays / 365 * leaveDays);
    if (intervals === 0) return [];

    const dates: Date[] = [];
    for (let i = 1; i <= intervals; i++) {
      const offsetDays = Math.floor(i * 365 / leaveDays);
      const date = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + offsetDays);
      dates.push(date);
    }
    return dates;
  }

  private daysBetween(start: Date, end: Date): number {
    return Math.floor((end.getTime() - start.getTime()) / 86400000);
  }

  /**
   * 确保第二年员工有上一年的释放计划，如果缺失则补算并保存。
   */
  private async ensureLastYearPlan(
    userId: string,
    empName: string,
    joinDateStr: string,
    workStartDateStr: string,
    currentYear: number,
    lastYearDict: Record<string, AnnualLeaveReleasePlanRow>,
    logger: SyncLogger,
  ): Promise<number> {
    const joinDateObj = this.parseDateOnly(joinDateStr);
    if (currentYear !== joinDateObj.getFullYear() + 1) return 0;

    if (lastYearDict[userId]) {
      return this.calculateBiasFromLastYear(lastYearDict[userId], joinDateObj, currentYear);
    }

    logger.log(`员工${empName}缺少${currentYear - 1}年计划，自动补算`);
    const lastYear = currentYear - 1;
    const leaveDates = this.calculateLeaveDates(joinDateStr, workStartDateStr, 0, lastYear);
    const planData = this.buildAnnualLeavePlanData(userId, leaveDates, lastYear, {
      adjustmentDays: 0,
      notCountDays: 0,
      employeeName: empName,
    });

    const planRepo = (this.prisma as any).dingtalkAnnualLeaveReleasePlan;
    const savedPlan = await planRepo.upsert({
      where: { userId_year: { userId, year: lastYear } },
      update: planData,
      create: planData,
    }) as AnnualLeaveReleasePlanRow;
    lastYearDict[userId] = savedPlan;

    return this.calculateBiasFromLastYear(savedPlan, joinDateObj, currentYear);
  }

  private calculateBiasFromLastYear(lastYearData: AnnualLeaveReleasePlanRow, joinDateObj: Date, currentYear: number): number {
    const dateObjs = this.getReleaseDatesFromPlan(lastYearData);
    if (dateObjs.length > 0) {
      const maxDate = new Date(Math.max(...dateObjs.map(d => d.getTime())));
      return -Math.floor((new Date(currentYear, 0, 1).getTime() - maxDate.getTime()) / 86400000);
    }
    return -Math.floor((new Date(currentYear, 0, 1).getTime() - joinDateObj.getTime()) / 86400000);
  }

  private buildAnnualLeavePlanData(
    userId: string,
    leaveDates: string[],
    year: number,
    options: { adjustmentDays: number; notCountDays: number; employeeName?: string },
  ): Record<string, unknown> {
    return {
      userId,
      employeeName: options.employeeName || '',
      year,
      adjustmentDays: options.adjustmentDays || 0,
      notCountDays: options.notCountDays || 0,
      totalDays: leaveDates.length,
      releaseSchedule: leaveDates.map((releaseDate, index) => ({
        dayIndex: index + 1,
        releaseDate,
      })),
      lastCalculatedAt: new Date(),
    };
  }

  private getReleaseDatesFromPlan(plan: AnnualLeaveReleasePlanRow): Date[] {
    const schedule = Array.isArray(plan.releaseSchedule) ? plan.releaseSchedule : [];
    return schedule
      .map((item: any) => String(item?.releaseDate || '').trim())
      .filter(Boolean)
      .map((dateStr: string) => this.parseDateOnly(dateStr));
  }

  private parseDateOnly(dateStr: string): Date {
    const parts = dateStr.split('-');
    return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
  }

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