import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { AdpApiService } from '../sdk/adp-api.service';
import { AdpSyncExecutionResult } from './adp-sync-result';
import { MeetingPtoMarkingService } from '@modules/meeting-attendance/services/meeting-pto-marking.service';
import { convertToUTC } from '@modules/meeting-attendance/utils/timezone-utils';

const DEFAULT_LOOKBACK_DAYS = 7;
const DEFAULT_LOOKAHEAD_DAYS = 35;
const MAX_WINDOW_DAYS = 42; // ADP 6 周限制
// ADP 默认 tier 单节点限制：75 calls/min ≈ 1.25 req/s（4 节点合计 5 req/s）。
// gateway 路由不均时连续请求可能落同一节点，触发单节点限流。
// 800ms 间隔 ≈ 1.25 req/s，保证最坏情况下单节点也不超限。
// 详见 docs (ADP Web API Gateway Rate Limit Policy, 2021)。
const REQUEST_DELAY_MS = 800;

// ADP time-off-requests 返回的 dateTimePeriod 用 ISO `-00:00` floating time 表达，
// 实际是"员工本地时间"（FF 总部 LA），需要按 LA 时区转 UTC 才能跟会议时段做重叠比较。
const ADP_DEFAULT_TZ = 'America/Los_Angeles';

/** ADP 的 `-00:00` 是 ISO 8601 "unknown local offset"，不是 UTC；剥掉后按 LA 时区解析 */
function parseAdpFloatingTime(s: string): Date {
  const stripped = s.replace(/(?:Z|[+-]\d{2}:?\d{2})$/, '');
  return convertToUTC(stripped, ADP_DEFAULT_TZ);
}

/**
 * PTO 同步：仅同步 approved 状态。
 * 取消/驳回的 PTO 通过本地"未见即删"实现硬删除（不在表里留 status 字段）。
 */
@Injectable()
export class AdpPtoSyncService {
  private readonly logger = new Logger(AdpPtoSyncService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly adpApi: AdpApiService,
    private readonly meetingPtoMarking: MeetingPtoMarkingService,
  ) {}

  /**
   * @param windowStartDays 窗口起点（相对今天），默认 -7
   * @param windowEndDays 窗口终点（相对今天），默认 +35
   */
  async run(
    windowStartDays = -DEFAULT_LOOKBACK_DAYS,
    windowEndDays = DEFAULT_LOOKAHEAD_DAYS,
  ): Promise<AdpSyncExecutionResult> {
    const startedAt = Date.now();
    const errors: string[] = [];
    const logLines: string[] = [];

    if (windowEndDays - windowStartDays > MAX_WINDOW_DAYS) {
      const msg = `窗口 ${windowStartDays}d ~ ${windowEndDays}d 超过 ADP 限制 ${MAX_WINDOW_DAYS} 天`;
      return {
        success: false,
        duration: Date.now() - startedAt,
        errors: [msg],
        logs: `[ERROR] ${msg}`,
      };
    }

    const today = new Date();
    today.setUTCHours(0, 0, 0, 0);
    const windowStart = new Date(today);
    windowStart.setUTCDate(windowStart.getUTCDate() + windowStartDays);
    const windowEnd = new Date(today);
    windowEnd.setUTCDate(windowEnd.getUTCDate() + windowEndDays);

    logLines.push(
      `[ADP] PTO 同步窗口: ${windowStart.toISOString().slice(0, 10)} ~ ${windowEnd.toISOString().slice(0, 10)}`,
    );

    let upsertedCount = 0;
    let deletedCount = 0;
    let userCount = 0;
    let entryFetchedCount = 0;

    try {
      const linkedUsers = await this.prisma.user.findMany({
        where: {
          status: 'ACTIVE',
          adpAoid: { not: null },
        },
        select: { id: true, adpAoid: true },
      });
      userCount = linkedUsers.length;
      logLines.push(`[ADP] 已 link User: ${userCount} 个`);

      // 节流：每用户间隔 REQUEST_DELAY_MS 避免触发 ADP 单节点 1.25 req/s 限流
      for (let i = 0; i < linkedUsers.length; i++) {
        const user = linkedUsers[i];
        if (!user.adpAoid) continue;
        if (i > 0) await new Promise((resolve) => setTimeout(resolve, REQUEST_DELAY_MS));

        let approvedRequests;
        try {
          approvedRequests = await this.adpApi.fetchTimeOffRequests(
            user.adpAoid,
            'approved',
            windowStart,
            windowEnd,
          );
        } catch (error: any) {
          errors.push(`user=${user.id} aoid=${user.adpAoid} fetch 失败: ${error.message}`);
          continue;
        }

        // 把 ADP 返回的所有 entry 按 (startTime, endTime) 去重：
        // ADP 偶尔会返回同一时段的两个 approved request（用户先后申请未自动作废），
        // 业务上等价于"那天 PTO"，只需保留一条。按 adpEntryId 字典序最小保留，
        // 保证每次同步选同一条，不会让 DB 里 entry_id 来回漂移。
        type Slot = {
          adpEntryId: string;
          start: Date;
          end: Date;
          leaveDate: Date;
        };
        const slotByTime = new Map<string, Slot>();
        for (const req of approvedRequests) {
          for (let entryIdx = 0; entryIdx < (req.timeOffEntries ?? []).length; entryIdx++) {
            const entry = req.timeOffEntries![entryIdx];
            const startStr = entry.dateTimePeriod?.startDateTime;
            const endStr = entry.dateTimePeriod?.endDateTime;
            if (!startStr || !endStr) continue;
            const start = parseAdpFloatingTime(startStr);
            const end = parseAdpFloatingTime(endStr);
            if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) continue;

            const adpEntryId = `${req.timeOffRequestID}_${entryIdx}`;
            const key = `${start.toISOString()}|${end.toISOString()}`;
            const leaveDate = new Date(start);
            leaveDate.setUTCHours(0, 0, 0, 0);

            const existing = slotByTime.get(key);
            if (!existing || adpEntryId < existing.adpEntryId) {
              slotByTime.set(key, { adpEntryId, start, end, leaveDate });
            }
          }
        }

        // 收集本次见到的 entry IDs（窗口内用于 delete-not-seen）
        const seenEntryIds = new Set<string>();
        for (const slot of slotByTime.values()) {
          seenEntryIds.add(slot.adpEntryId);
          entryFetchedCount++;
          try {
            await this.prisma.adpPtoSchedule.upsert({
              where: {
                userId_adpEntryId: { userId: user.id, adpEntryId: slot.adpEntryId },
              },
              update: {
                leaveDate: slot.leaveDate,
                startTime: slot.start,
                endTime: slot.end,
                syncedAt: new Date(),
              },
              create: {
                userId: user.id,
                adpAoid: user.adpAoid,
                leaveDate: slot.leaveDate,
                startTime: slot.start,
                endTime: slot.end,
                adpEntryId: slot.adpEntryId,
              },
            });
            upsertedCount++;
          } catch (e: any) {
            errors.push(`user=${user.id} entry=${slot.adpEntryId} upsert 失败: ${e.message}`);
          }
        }

        // delete-not-seen：窗口内本地存在但 ADP 没返回的，视为已取消/移动，硬删
        const stale = await this.prisma.adpPtoSchedule.findMany({
          where: {
            userId: user.id,
            startTime: { gte: windowStart, lt: windowEnd },
            ...(seenEntryIds.size > 0 ? { adpEntryId: { notIn: Array.from(seenEntryIds) } } : {}),
          },
          select: { id: true },
        });

        if (stale.length > 0) {
          await this.prisma.adpPtoSchedule.deleteMany({
            where: { id: { in: stale.map((s) => s.id) } },
          });
          deletedCount += stale.length;
        }
      }

      logLines.push(
        `[ADP] entry 拉取 ${entryFetchedCount} 条，upsert ${upsertedCount}，删除 ${deletedCount}`,
      );
      if (errors.length > 0) {
        logLines.push(`[ADP] 错误 ${errors.length} 条（详见 error 字段）`);
      }

      // 后置补救：扫描所有"PTO 时段内 + 未来"的会议，对必参会人触发 PTO 标记。
      // 解决 series.service / templates.service / outlook-sync.service 直接 prisma.meeting.create
      // 没有挂 fire-and-forget 钩子，导致系列会议自动标记失败的问题。
      try {
        const meetingsToMark = await this.prisma.meeting.findMany({
          where: {
            status: { not: 'CANCELLED' },
            startTime: { lt: windowEnd },
            endTime: { gt: windowStart },
            requiredAttendees: {
              some: {
                user: {
                  adpAoid: { not: null },
                  adpPtoSchedules: {
                    some: {
                      startTime: { lt: windowEnd },
                      endTime: { gt: windowStart },
                    },
                  },
                },
              },
            },
          },
          select: { id: true },
        });
        for (const m of meetingsToMark) {
          this.meetingPtoMarking.applyForMeetingFireAndForget(m.id);
        }
        logLines.push(`[ADP] 触发 ${meetingsToMark.length} 个会议补标 PTO（后置）`);
      } catch (e: any) {
        this.logger.warn(`后置补标失败: ${e.message}`);
      }

      return {
        success: errors.length === 0,
        duration: Date.now() - startedAt,
        errors,
        logs: logLines.join('\n'),
        recordsFetched: entryFetchedCount,
        recordsUpserted: upsertedCount,
        recordsDeleted: deletedCount,
        extras: { userCount },
      };
    } catch (error: any) {
      this.logger.error(`PTO 同步失败: ${error.message}`);
      errors.push(error.message);
      return {
        success: false,
        duration: Date.now() - startedAt,
        errors,
        logs: logLines.concat(`[ERROR] ${error.message}`).join('\n'),
        recordsFetched: entryFetchedCount,
        recordsUpserted: upsertedCount,
        recordsDeleted: deletedCount,
      };
    }
  }
}
