/**
 * Priority Scorer — 客户优先级排序模型(v3.0 业务口径)
 *
 * 对齐《客户画像标签字典 v3.0》第三部分"客户优先级排序模型"。
 *
 * 综合 = 急迫性×0.4 + 价值性×0.3 + 意愿度×0.3   (三维各 0-10 → ×10 映射 0-100)
 *   急迫性 = urgency_level(紧急10/高7/中4/低1/无0)— 纯客观"病多急",不看人
 *   价值性 = 潜在治疗类型 + 牙数 → 预估收入(纯客观,不看人有没有钱)
 *   意愿度 = RFM依从×0.375 + 主诉行为×0.375 + 信任基础×0.25
 *     (v3.0 原含"触达活跃×0.2";PAC 无企微/小程序/呼叫/推送数据源 → 去掉,余三项按比例归一)
 *
 * ⚠️ 改 scorer 只改 priorityScore(排序),不改召回候选集(gap 选择独立)→ verify-recall 不受影响。
 * ⚠️ 已知科学局限(v3.0 同):权重人拍(业务 baseline,可调);意愿度≈倾向性非增益(uplift),
 *    会偏好"本来就会来的人"——要做对需试点留随机对照组(follow-up)。
 *
 * breakdown 落 plan_reasons,详情页展示客服"为什么这个分"。
 */

// ── 价值性:潜在治疗类型 → 预估收入分(0-10,v3.0 表)──
//   种植按牙数分档(单颗8/多颗9/半口+10);修复按牙数(<3=5/≥3=6);其余定值。
//   按召回 primaryCode 映射(单一真理源在此,跟 v3.0 价值表对齐)。
const VALUE_BASE_BY_CODE: Record<string, number> = {
  K08: 8, // 种植(单颗 base;多颗/半口在 computeValue 提分)
  K07: 7, // 正畸 / 早矫
  K04: 6, // 根管(+冠)
  K03: 5, // 牙体修复(冠/贴面;≥3颗提到 6)
  K09: 4, // 颌骨囊肿(必处理手术)
  K05: 5, // 牙周(全口序列)
  K06: 3, // 牙龈/牙槽嵴
  K01: 3, // 阻生牙拔除(低值,但可能是种植入口)
  K00: 3, // 发育/萌出
  K02: 2, // 龋齿充填
};

const URGENCY_SCORE: Record<string, number> = { urgent: 10, high: 7, mid: 4, low: 1 };

// key 对齐 rfm.feature 八象限 segment code
const RFM_ADHERENCE: Record<string, number> = {
  important_value: 10, // 重要价值
  important_retain: 8, // 重要保持
  important_develop: 8, // 重要发展
  important_winback: 5, // 重要挽留
  general_value: 6, // 一般价值
  general_retain: 5, // 一般保持
  general_develop: 5, // 一般发展
  low_active: 2, // 低活跃
};

const TRUST_BASE: Record<string, number> = {
  mature: 9, // 成熟客
  growth: 7, // 成长客
  new: 4, // 新客
  reactivate: 3, // 待激活
  prospect: 2, // 潜客
  dormant: 1, // 沉睡客
  churned: 0, // 流失客
};

export interface PriorityInput {
  /// C.2.1 急迫等级(urgency_level 特征)
  urgencyLevel: 'urgent' | 'high' | 'mid' | 'low' | null;
  /// 自诊断/建议至今天数(诊断新鲜度衰减用)
  daysSince: number;
  /// 该病种黄金窗上界(天,DiagnosisTreatmentMap.windowDays)— 新鲜度衰减锚点
  windowDays: number;
  /// 召回子场景 primaryCode(K08/K07/...)— 决定价值
  primaryCode: string;
  /// 剩余未治牙位数(种植/修复分档用;全口码为 0)
  toothCount: number;
  /// rfm 分群 code(important_value/...)
  rfmSegment: string | null;
  /// 主诉行为:患者咨询过该类治疗(consultation 意向命中本 gap 类别)
  consultIntentMatch: boolean;
  /// 信任:lifecycle 阶段 code
  lifecycleStage: string | null;
  /// 信任 +1:同类治疗史(跨科室信任迁移)
  hasTreatmentHistory: boolean;
  /// 信任 +1:转介绍达人
  isReferralChampion: boolean;
  /// 信任 +1:近1年到诊 且 履约良好(非屡次爽约)
  recentVisitGood: boolean;
  /// 信号置信度(cluster 最优来源):医生诊断 1.0 / 影像AI 0.85 / 建议 0.8 / 未知 0.9
  ///   v3.0 C.1.1:病历诊断 100% > 影像AI 70-90% > 建议/EMR语义。低置信(影像/建议)降权,防误召冲高分。
  confidence: number;
}

export interface PriorityBreakdown {
  urgency: number; // 急迫性 0-10
  value: number; // 价值性 0-10
  willingness: number; // 意愿度 0-10
  rfmAdherence: number; // 意愿·RFM依从
  intentBehavior: number; // 意愿·主诉行为
  trustBase: number; // 意愿·信任基础
  freshness: number; // 诊断新鲜度因子 0.4-1.0(老诊断衰减)
  confidenceFactor: number; // 置信度因子 = 来源置信度(诊断1.0/建议0.8/影像0.5)
  base: number; // 三维加权(未乘因子)
  raw: number; // 综合 0-10(× 新鲜度 × 置信度后)
}

/// 置信度因子 = 来源置信度直用(诊断1.0 / 建议0.8 / 影像AI0.5),下限 0.5 防过罚。
///   v3.1:三源标定后因子与来源置信度一致,显示与打分同一套值(见 sourceConfidence)。
export function computeConfidenceFactor(confidence: number): number {
  return Math.max(0.5, Math.min(1, confidence));
}

/**
 * 诊断新鲜度因子(0.4-1.0)— 老诊断止损。
 *   黄金窗内(daysSince ≤ windowDays):1.0(新鲜,满权重)
 *   过窗:从 windowDays 线性衰减,到 2×windowDays 降至 0.4,之后地板 0.4。
 * 理由:入池无时间上界(临床缺口不自愈仍入池),但"很多年前的诊断"可信度/可行动性低
 *   (可能已在他处处理 / 情况变化)→ 软衰减压低优先级,不硬切(同 W3 设计:止损交 scorer)。
 *   per-病种:K08 缺牙窗 180、K07 正畸窗 365 —— 慢病衰减得慢,合理。
 */
export function computeFreshness(daysSince: number, windowDays: number): number {
  if (daysSince <= windowDays) return 1.0;
  const t = Math.min(1, (daysSince - windowDays) / Math.max(1, windowDays));
  return 1.0 - 0.6 * t;
}

export interface PriorityResult {
  score: number; // 0-100
  breakdown: PriorityBreakdown;
}

function computeValue(primaryCode: string, toothCount: number): number {
  const base = VALUE_BASE_BY_CODE[primaryCode] ?? 3;
  if (primaryCode === 'K08') {
    // 种植:单颗8 / 多颗(2-5)9 / 半口+(≥6)10
    if (toothCount >= 6) return 10;
    if (toothCount >= 2) return 9;
    return 8;
  }
  if (primaryCode === 'K03' && toothCount >= 3) return 6; // 修复 ≥3颗
  return base;
}

function computeWillingness(input: PriorityInput): {
  willingness: number;
  rfmAdherence: number;
  intentBehavior: number;
  trustBase: number;
} {
  // RFM 依从
  const rfmAdherence = input.rfmSegment ? (RFM_ADHERENCE[input.rfmSegment] ?? 5) : 5;
  // 主诉行为:咨询过该类→8;仅诊断被动→2(召回候选已排除"已预约该项目",故无 10 档)
  const intentBehavior = input.consultIntentMatch ? 8 : 2;
  // 信任基础:lifecycle + 同类治疗史 +1 + 转介 +1 + 近1年到诊履约良好 +1(封顶 10)
  let trustBase = input.lifecycleStage ? (TRUST_BASE[input.lifecycleStage] ?? 4) : 4;
  if (input.hasTreatmentHistory) trustBase += 1;
  if (input.isReferralChampion) trustBase += 1;
  if (input.recentVisitGood) trustBase += 1;
  trustBase = Math.min(10, trustBase);
  // 权重(触达活跃 0.2 去掉,RFM 0.3 / 主诉 0.3 / 信任 0.2 归一到 0.375/0.375/0.25)
  const willingness = rfmAdherence * 0.375 + intentBehavior * 0.375 + trustBase * 0.25;
  return { willingness, rfmAdherence, intentBehavior, trustBase };
}

/**
 * 主算分入口 — v3.0 三维模型。score 落 hit.priorityScore,breakdown 落 plan_reasons。
 */
export function calcPriority(input: PriorityInput): PriorityResult {
  const urgency = input.urgencyLevel ? (URGENCY_SCORE[input.urgencyLevel] ?? 0) : 0;
  const value = computeValue(input.primaryCode, input.toothCount);
  const { willingness, rfmAdherence, intentBehavior, trustBase } = computeWillingness(input);
  const freshness = computeFreshness(input.daysSince, input.windowDays);
  const confidenceFactor = computeConfidenceFactor(input.confidence);

  const base = urgency * 0.4 + value * 0.3 + willingness * 0.3;
  const raw = base * freshness * confidenceFactor; // 老诊断衰减 × 影像AI/建议 降权

  // ⭐ score 与 breakdown.raw **同源同精度**,别再取整。
  //
  // 原来是 `Math.round(raw * 10)` —— 排序键退化成 0-100 的整数,而列表展示的是
  // raw 的两位小数(0-10),**排序精度比展示精度粗 10 倍**。后果:同一个整数档里
  // 塞着一堆不同的展示分,档内只能按 created_at 兜底排,客服看到的就是
  // 「3.12 → 3.10 → 3.10 → 3.12」这种乱序。
  // 2026-07-29 测试服实测:priority_score=31 一档里有 21 种展示分(3.20~2.59);
  // 全池 126,073 张工单只有 93 个排序档位、却有 849 种展示分。
  //
  // 现在:raw 先按展示精度定死两位小数,score = 该值 × 10 → 两者严格 1:1,
  // 「排序键 === 展示值 × 10」,档内不再有隐藏差异。列仍是 Float(schema 本来就是),
  // 量纲仍是 0-100(queueStats 的 70/40 分档、前端进度条 score/100 全部不受影响)。
  const rawDisplay = Math.round(raw * 100) / 100; // 0-10,两位小数 = 列表展示的那个数
  const score = Math.max(0, Math.min(100, Math.round(rawDisplay * 1000) / 100));

  return {
    score,
    breakdown: {
      urgency,
      value,
      willingness: Math.round(willingness * 100) / 100,
      rfmAdherence,
      intentBehavior,
      trustBase,
      freshness: Math.round(freshness * 100) / 100,
      confidenceFactor,
      base: Math.round(base * 100) / 100,
      raw: rawDisplay, // 与 score 同一个数(score = rawDisplay × 10),别各算各的
    },
  };
}
