/**
 * 「下一次会因为时间流逝而改档」的时刻 —— 各 extractor 的 nextBoundaryAt 用这几个原语拼。
 *
 * ── 解决什么问题 ──
 * 水位闸只在**事实变了**时才放行重算(见 PersonaService.isFresh),这是对的:没有它,
 * 40 万患者每天全量重算纯属烧钱。但有一类变化不伴随任何事实变动 —— 时间本身走了:
 *   · 急迫等级跨 30 / 90 天线      · 生命周期跨 180 / 540 / 730 天线
 *   · RFM 的 R 分跨 540/730/1095/1460 天线   · 年龄跨段、种植年龄禁忌满 19 岁解除
 *   · 时间偏好 / 特别关注的滑动窗口把最老那条记录挤出去
 * 这类患者在旧实现里永远不会被重算,画像就一直停在旧档。
 *
 * ── 为什么不靠"每天扫一遍" ──
 * 那等于把闸拆了。正确做法是**算出确切的到期时刻**存进 personas.next_boundary_at,
 * 每晚只捞 `next_boundary_at <= now()` 那一小撮 —— 精确、可索引,量级是每天几百到几千,
 * 而不是 40 万。
 *
 * 约定:只返回**未来**时刻;已经全部过去 → null(表示"再等下去也不会变了")。
 */

const DAY_MS = 86400_000;

/** 取最早的那个未来时刻;没有未来项 → null */
export function earliestFuture(now: Date, ...candidates: Array<Date | null | undefined>): Date | null {
  let best: Date | null = null;
  for (const c of candidates) {
    if (!c || c.getTime() <= now.getTime()) continue;
    if (!best || c.getTime() < best.getTime()) best = c;
  }
  return best;
}

/**
 * 患者满 `ages` 中下一个年龄的那天(即生日)。
 *
 * 与 ageYearsAt 同语义:生日当天算作已满。2/29 出生者用 setFullYear 会滚到 3/1,
 * 早一天触发重算 —— 宁可早一天,不可漏。
 */
export function nextAgeBoundary(
  birth: Date | null,
  now: Date,
  ages: readonly number[],
): Date | null {
  if (!birth) return null;
  const sorted = [...ages].sort((a, b) => a - b);
  for (const age of sorted) {
    const at = new Date(birth);
    at.setFullYear(at.getFullYear() + age);
    if (at.getTime() > now.getTime()) return at;
  }
  return null; // 全部年龄档都已跨过
}

/** anchor 之后 `dayThresholds` 天中,第一个还在未来的时刻 */
export function nextDayBoundary(
  anchor: Date | null,
  now: Date,
  dayThresholds: readonly number[],
): Date | null {
  if (!anchor) return null;
  const sorted = [...dayThresholds].sort((a, b) => a - b);
  for (const d of sorted) {
    const at = new Date(anchor.getTime() + d * DAY_MS);
    if (at.getTime() > now.getTime()) return at;
  }
  return null; // 最大阈值也已跨过 → 再等也不改档
}

/**
 * 滑动窗口口径(近 N 天)下,**窗内最早那条记录滑出窗口**的时刻。
 *
 * 一旦它滑出,窗内统计量(计数 / 占比)就会变 —— 可能让标签出现或消失。
 * 窗内无记录 → null(没东西可过期)。
 */
export function nextWindowExpiry(
  times: Array<Date | null | undefined>,
  windowDays: number,
  now: Date,
): Date | null {
  const windowMs = windowDays * DAY_MS;
  const since = now.getTime() - windowMs;
  let oldest: number | null = null;
  for (const t of times) {
    if (!t) continue;
    const ms = t.getTime();
    if (ms < since || ms > now.getTime()) continue; // 窗外 / 未来记录不参与
    if (oldest === null || ms < oldest) oldest = ms;
  }
  return oldest === null ? null : new Date(oldest + windowMs);
}
