/**
 * Priority Scorer — 6 因子召回优先级算分(v2.1)
 *
 * 对齐 docs/algorithm/potential-treatment-recall.md §8.2 算法 v2。
 *
 * 公式:
 *   raw   = (base × timeWindowFactor + valueBonus + likelihoodBonus + urgencyBonus) × confidenceFactor
 *   score = clamp(round(raw), 0, 100)
 *
 * 6 因子语义:
 *   1. ClinicalBase     — 子场景临床基线(缺牙 60 / 龋齿 45 / 牙周 50 / ...)
 *   2. TimeWindowFactor — 黄金窗内 1.0,过早 0.6,过晚衰减到 0.4
 *   3. ValueBonus       — patient LTV(Persona value 特征)
 *   4. LikelihoodBonus  — 转化可能性(recall_risk 低 + 近期回访成功)
 *   5. UrgencyBonus     — 临床紧迫(诊断超 X 天 → 邻牙倾斜 / 复发风险升)
 *   6. SignalQualityDiscount — LLM 抽出来的低置信信号降权
 *
 * 详情页 breakdown 用 calcPriority(...).breakdown 落库到 plan_reasons.evidence,
 * UI 展示客服"为什么这个 73 分"的可解释性来源。
 */

export interface PriorityInput {
  /// 子场景临床基线(各 scenario 自己提供 60 / 55 / 50 等)
  base: number;
  /// 自诊断 / 推荐至今的天数
  daysSince: number;
  /// 黄金窗:[start, end](天数);daysSince 落 [start, end] = factor 1.0
  goldenRange: [number, number];
  /// Persona feature 的 value.score(0-4 档,见 ValueFeatureExtractor),null = 无 persona
  valueScore: number | null;
  /// Persona feature 的 recall_risk.score(0-3 档),null = 无 persona
  riskScore: number | null;
  /// 该 patient 近 30 天的 plan_executions outcome 列表(可空)
  recentExecutions: Array<{ outcome: string }>;
  /// 各信号源 confidence(0-1);LLM 抽的 < 1,结构化诊断 = 1
  signalConfidences: number[];
  /// 子场景定义的"临床紧迫"临界天数(超过加 urgencyBonus)
  urgencyDayThreshold?: number;
}

export interface PriorityBreakdown {
  /// 客服 UI 直接显示:"临床基线 60 × 0.85 = 51"
  clinicalBase: number;
  timeWindowFactor: number;
  main: number; // base × timeWindowFactor

  valueBonus: number;
  likelihoodBonus: number;
  urgencyBonus: number;
  confidenceFactor: number; // 1.0 / 0.9 / 0.75 等
  raw: number; // 未 clamp 未 round 的真实数
}

export interface PriorityResult {
  score: number;
  breakdown: PriorityBreakdown;
}

/**
 * 主算分入口 — 6 因子组合。
 *
 * 调用方:Scenario plugin 算出 ScenarioHit 时调本函数,score 落 hit.priorityScore,
 *         breakdown 序列化到 plan_reasons.evidence(JSONB)便于 UI 详情页显示。
 */
export function calcPriority(input: PriorityInput): PriorityResult {
  // ─ 1. TimeWindowFactor ─
  const timeWindowFactor = computeTimeWindowFactor(input.daysSince, input.goldenRange);
  const main = input.base * timeWindowFactor;

  // ─ 2. ValueBonus(value.score: 0=新客 / 1=普通 / 2=银卡 / 3=金卡 / 4=钻卡)─
  const valueBonus = computeValueBonus(input.valueScore);

  // ─ 3. LikelihoodBonus(risk 越低,触达成功率越高;近期 success 加分)─
  const likelihoodBonus = computeLikelihoodBonus(
    input.riskScore,
    input.recentExecutions,
  );

  // ─ 4. UrgencyBonus(超过临床晋级临界即加固定分)─
  const urgencyBonus = computeUrgencyBonus(
    input.daysSince,
    input.urgencyDayThreshold,
  );

  // ─ 5. SignalQualityDiscount(最低 confidence 决定整体置信因子)─
  const confidenceFactor = computeConfidenceFactor(input.signalConfidences);

  const raw =
    (main + valueBonus + likelihoodBonus + urgencyBonus) * confidenceFactor;
  const score = Math.max(0, Math.min(100, Math.round(raw)));

  return {
    score,
    breakdown: {
      clinicalBase: input.base,
      timeWindowFactor,
      main,
      valueBonus,
      likelihoodBonus,
      urgencyBonus,
      confidenceFactor,
      raw,
    },
  };
}

// ─────────────────────────────────────────────────────────
// 因子计算函数(集中此处,业务方调阈值改这里)
// ─────────────────────────────────────────────────────────

/**
 * TimeWindow 因子曲线:
 *   过早(< start):线性 0.6 → 1.0
 *   黄金窗(start ≤ d ≤ end):1.0
 *   过晚(> end):线性 1.0 → 0.4(在 2×end 处衰减完)
 */
export function computeTimeWindowFactor(
  daysSince: number,
  range: [number, number],
): number {
  const [start, end] = range;
  if (daysSince < 0) return 0.4; // 数据脏的兜底
  if (daysSince < start) {
    // 过早:线性从 0.6 升到 1.0
    const t = daysSince / Math.max(1, start);
    return 0.6 + 0.4 * t;
  }
  if (daysSince <= end) return 1.0;
  // 过晚:从 end 起,2×end 时降到 0.4
  const overdue = daysSince - end;
  const decayWindow = end; // 2×end - end = end
  const t = Math.min(1, overdue / decayWindow);
  return 1.0 - 0.6 * t;
}

export function computeValueBonus(valueScore: number | null): number {
  const s = valueScore ?? 0;
  if (s >= 4) return 20; // 钻卡
  if (s >= 3) return 15; // 金卡
  if (s >= 2) return 10; // 银卡
  if (s >= 1) return 5; // 普通付费
  return 0;
}

export function computeLikelihoodBonus(
  riskScore: number | null,
  recentExecutions: Array<{ outcome: string }>,
): number {
  // recall_risk:0=none / 1=low / 2=medium / 3=high — 越高触达可能性越低
  const risk = riskScore ?? 1;
  const riskBonus = Math.max(0, Math.round((3 - risk) * 2)); // 0~6
  // 仅"真实正向进展"加权:成功转化 / 改约(都已敲定新时间)。
  //   注:'considering'(考虑中)已移除 — 软意向不该把患者顶到列表最前天天打;
  //   且 considering 现在走 snoozedUntil 冷静期,不应再额外加权(否则与抑制窗自相矛盾)。
  const recentSuccessBonus = recentExecutions.some((e) =>
    ['success_appointed', 'rescheduled'].includes(e.outcome),
  )
    ? 4
    : 0;
  return Math.min(10, riskBonus + recentSuccessBonus);
}

export function computeUrgencyBonus(
  daysSince: number,
  threshold: number | undefined,
): number {
  if (!threshold) return 0;
  if (daysSince <= threshold) return 0;
  // 超过临界,固定加 5 分(简化;真要分级再扩)
  return 5;
}

export function computeConfidenceFactor(confidences: number[]): number {
  if (confidences.length === 0) return 1.0;
  const min = Math.min(...confidences);
  if (min >= 0.9) return 1.0;
  if (min >= 0.7) return 0.9;
  return 0.75;
}
