/**
 * PAC Canonical Codes — 集团统一字典(单一真理源)
 *
 * **定位**:所有 fact.content 里出现的 enum 值,必须 ∈ 本文件定义的闭集。
 * 宿主侧任意码(`A01` / `诊002` / `龋齿`)→ Layer A yaml `enum_mapping` 翻译到此处。
 *
 * **维护流程**:新增码走 PR,至少 1 牙医 + 1 业务方 + 1 PAC 工程 review。
 * 必填:含义 / 临床触发场景 / 算法用法 / 中文名。
 *
 * 详见 `docs/algorithm/canonical-fact-layer.md` §三。
 */
import { z } from 'zod';

// =============================================================
// 诊断码(PACDiagnosisCode)— 借 ICD-10 K00-K14 大类 + 业务码 + 推荐码
// =============================================================

/**
 * 诊断码 — 大类粒度(不细分到 K02.1 等亚分类,算法用大类够用)
 *
 * 三段:
 *   1. 临床主类(K02-K14)— 沿用 ICD-10 命名,便于跟标准接轨
 *   2. PAC 业务码(治疗失败 / 投诉相关)— ICD 不分但算法需要
 *   3. 推荐码(EMR 文本抽取产物)— Layer C 产 recommendation_record 时用
 */
export const PACDiagnosisCodes = {
  // ── 临床主类(ICD-10 K00-K14 借用)──
  K00: { nameZh: '牙发育和萌出障碍', icd10Range: 'K00.*' },
  K01: { nameZh: '阻生 / 埋伏牙', icd10Range: 'K01.*' },
  K02: { nameZh: '龋病', icd10Range: 'K02.*' },
  K03: { nameZh: '牙体硬组织其他疾病', icd10Range: 'K03.*' },
  K04: { nameZh: '牙髓 / 根尖周炎', icd10Range: 'K04.*' },
  K05: { nameZh: '牙周炎 / 牙龈炎', icd10Range: 'K05.*' },
  K06: { nameZh: '牙龈 / 牙槽骨疾患', icd10Range: 'K06.*' },
  K07: { nameZh: '牙颌面异常', icd10Range: 'K07.*' },
  K08: { nameZh: '牙列丢失 / 缺牙', icd10Range: 'K08.*' },
  K09: { nameZh: '颌骨囊肿', icd10Range: 'K09.*' },
  K10: { nameZh: '颌骨其他疾病', icd10Range: 'K10.*' },
  K11: { nameZh: '唾液腺疾患', icd10Range: 'K11.*' },
  K12: { nameZh: '口腔黏膜疾患', icd10Range: 'K12.*' },
  K13: { nameZh: '唇 / 口腔黏膜其他疾患', icd10Range: 'K13.*' },
  K14: { nameZh: '舌疾患', icd10Range: 'K14.*' },

  // ── PAC 业务码 ──
  IMPLANT_FAILURE: { nameZh: '种植失败 / 周围炎', business: true as const },
  ORTHO_RELAPSE: { nameZh: '正畸反弹', business: true as const },
  ENDO_FRACTURE: { nameZh: '根管牙折裂', business: true as const },

  // ── 推荐码(EMR 文本抽取产物 / recommendation_record.content.code)──
  // 跟 K0x scenario 一一对应,Layer C LLM 抽取上线后立即起效
  IMPLANT_RECOMMENDED: { nameZh: '建议种植' },                          // → K08 missing_tooth
  CROWN_RECOMMENDED: { nameZh: '建议戴冠' },                            // → K08 / K04 后续
  FILLING_RECOMMENDED: { nameZh: '建议充填' },                          // → K02 caries
  SRP_RECOMMENDED: { nameZh: '建议牙周基础治疗' },                       // → K05 perio
  EXTRACTION_RECOMMENDED: { nameZh: '建议拔除' },                       // → K01 impacted / K03 残根
  ANNUAL_REVIEW_RECOMMENDED: { nameZh: '建议年度复查' },                 // → post-treatment(K05/K08 复查)
  ORTHO_CONSULT_RECOMMENDED: { nameZh: '建议正畸咨询' },                 // → K07 ortho
  RCT_RECOMMENDED: { nameZh: '建议根管治疗' },                          // → K04 endo
  HARD_TISSUE_REPAIR_RECOMMENDED: { nameZh: '建议牙体修复' },           // → K03 楔缺/缺损 充填或嵌体
  GUM_TREATMENT_RECOMMENDED: { nameZh: '建议牙龈/牙槽嵴处置' },          // → K06 牙龈萎缩/增生/系带切除
  ERUPTION_INTERVENTION_RECOMMENDED: { nameZh: '建议萌出干预' },         // → K00 乳牙滞留拔除 / 多生牙 / 萌出助萌
  JAW_CYST_REMOVAL_RECOMMENDED: { nameZh: '建议囊肿摘除术' },            // → K09 颌骨囊肿
} as const;
export type PACDiagnosisCode = keyof typeof PACDiagnosisCodes;
export const PACDiagnosisCodeSchema = z.enum(
  Object.keys(PACDiagnosisCodes) as [PACDiagnosisCode, ...PACDiagnosisCode[]],
);

// =============================================================
// 治疗类别(PACTreatmentCategory)— 大类粒度
//
// 跟 enums/index.ts 的 TreatmentCategory 对齐,这里是 canonical 闭集 schema;
// content.category 的 enum 校验通过 PACTreatmentCategorySchema。
// =============================================================

export const PACTreatmentCategories = {
  endodontic: { nameZh: '牙髓 / 根管' },
  restorative: { nameZh: '充填 / 嵌体' },
  prosthodontic: { nameZh: '修复 / 冠桥' },
  implant: { nameZh: '种植' },
  periodontic: { nameZh: '牙周' },
  orthodontic: { nameZh: '正畸' },
  surgical: { nameZh: '外科' },
  pediatric: { nameZh: '儿牙' },
  preventive: { nameZh: '预防' },
  cosmetic: { nameZh: '美学' },
  /**
   * review — 复查 / 拆线 / 暂观 / 无治疗 等"流程性事实"(W3 末加)
   *
   * 实际数据出处:DW EMR.treat_plan.treatName 实测有大量
   *   "常规复查"(12k)/"复查"(2.5k)/"定期洁牙"(1.7k)/"检查"(0.7k)/
   *   "拆线"(1.5k)/"暂观"(1.8k)/"观察"(1.3k)/"无治疗"(1.5k)
   *
   * 设计意图:这些动作是真实事实(医生做了某个流程节点 / 临床判断本次不动手),
   * **不该塞 preventive(会污染"已做预防"判定),也不该丢弃(丢失事实)**。
   * 给独立 category,fact 层保真,scenario 算法可选择性消费。
   */
  review: { nameZh: '复查 / 流程' },
} as const;
export type PACTreatmentCategory = keyof typeof PACTreatmentCategories;

/**
 * 预约主诉类别 → PAC treatment category 映射(W3 末新加)
 *
 * 数据源:DW `fact_appointment_out.appo_complaint_category` 真实分布(8 大类 + 复合)
 *   常规 (467k) · 正畸 (138k) · 种植 (64k) · 儿科 (31k) · 修复 (21k) ·
 *   早矫 (13k) · 拔牙 (11k) · 牙周 (5.8k)
 *
 * 用途:chain-composer S2 判定 — 患者真"进入治疗链"的代理信号(医生侧 planned 不算,患者主动预约才算)
 *   例:K05 牙周链需要 appointment.complaint_category 含"牙周" 才算 S2 命中
 *
 * "常规"映射 preventive — 临床上 = 检查/洁牙(预防),不算任何治疗链的"已进入"信号
 * 复合值("常规,种植")host 用 "," 分隔,消费方 split 后逐个匹配
 */
export const APPT_COMPLAINT_TO_CATEGORY: Record<string, PACTreatmentCategory> = {
  常规: 'preventive',
  正畸: 'orthodontic',
  早矫: 'orthodontic',     // 儿童早期矫治
  种植: 'implant',
  儿科: 'pediatric',
  修复: 'prosthodontic',
  拔牙: 'surgical',
  牙周: 'periodontic',
};

/// 把 host 预约 complaint_category 字符串("牙周" / "常规,种植" 复合)解析成 PAC category 集合
export function parseComplaintCategories(raw: string | null | undefined): PACTreatmentCategory[] {
  if (!raw) return [];
  const out = new Set<PACTreatmentCategory>();
  for (const part of raw.split(/[,,]/).map((s) => s.trim()).filter(Boolean)) {
    const cat = APPT_COMPLAINT_TO_CATEGORY[part];
    if (cat) out.add(cat);
  }
  return [...out];
}
/// 咨询意向项目(fact_consult_out.potential_cure)→ PAC 治疗类别(单一真理源)。
/// ⚠️ 这是【患者主观意愿】(咨询时想做什么),区别于 diagnosis(医生客观诊断)/ recommendation(医生建议)。
///   仅入 consultation_record.content,**不进召回**(召回只认医生诊断/建议)。
export const CONSULT_INTENT_TO_CATEGORY: Record<string, PACTreatmentCategory> = {
  种植: 'implant',
  正畸: 'orthodontic',
  早矫: 'orthodontic',
  早期矫正: 'orthodontic',
  洁牙: 'preventive',
  涂氟: 'preventive',
  窝沟封闭: 'preventive',
  充填: 'restorative',
  补牙: 'restorative',
  修复: 'prosthodontic',
  智齿拔除: 'surgical',
  拔牙: 'surgical',
  拔功能牙: 'surgical',
  牙周: 'periodontic',
  根管: 'endodontic',
  复杂根管: 'endodontic',
  美白: 'cosmetic',
  儿科: 'pediatric',
  // '无' / 其他 → 不映射(非有效意向)
};

/// 解析 potential_cure 串(Python list repr,单引号:`['种植','洁牙']`)→ { intents 原文, categories 归一 }。
/// '无' 及未知值剔除;categories 去重。
export function parsePotentialCure(raw: string | null | undefined): {
  intents: string[];
  categories: PACTreatmentCategory[];
} {
  if (!raw) return { intents: [], categories: [] };
  const intents = raw
    .replace(/^\[|\]$/g, '')
    .split(',')
    .map((s) => s.trim().replace(/^['"]|['"]$/g, '').trim())
    .filter((s) => s && s !== '无');
  const categories = [...new Set(intents.map((i) => CONSULT_INTENT_TO_CATEGORY[i]).filter(Boolean))] as PACTreatmentCategory[];
  return { intents, categories };
}

export const PACTreatmentCategorySchema = z.enum(
  Object.keys(PACTreatmentCategories) as [
    PACTreatmentCategory,
    ...PACTreatmentCategory[],
  ],
);

// =============================================================
// 诊断/推荐码 → 期望治疗类别 + 临床黄金窗 + 漏治链显示名(单一真理源)
//
// ⭐ 单一收口(canonical-fact-layer.md 原则 3):
//   "某诊断该做什么治疗 / 多久内该启动 / 漏治链怎么显示" 只在这里定义一次。
//   下游 4 处引用,**不再各写一份**:
//     - persona/treatment-chain-status.feature.ts(用 categories 判缺口)
//     - persona/recall-risk.feature.ts(用 categories + windowDays 判超期缺口)
//     - plan/engine/chain-composer.service.ts(用 categories 排除 + chainLabel/wholeMouth 显示)
//     - plan/engine/scenarios/treatment-initiation-recall.scenario.ts(用 categories 作 excludeCats)
//
//   key = 诊断码(K0x)或推荐码(*_RECOMMENDED);value:
//     - categories            : 满足任一即视为"已治疗"(消缺口 / 排除召回)
//     - cooldownDays          : 入池下界 = 治疗考虑期(诊断后 < cooldownDays 不召回 / 不立链)
//     - windowDays            : 黄金窗上界(诊断后多少天内该启动治疗,超过即过窗,scorer 衰减)
//     - urgencyDayThreshold   : 临床紧迫临界(超过此天数额外加 urgencyBonus)
//     - chainLabel            : 漏治链 UI 显示名(chain-composer 用)
//     - wholeMouth            : 空牙位时显示"全口"(牙周类全口性疾病)否则"未标注牙位"
//
//   下游消费(单一真理源 — 不允许任一处再硬编码窗口/临界):
//     - plan/engine/scenarios/treatment-initiation-recall.scenario.ts(入池下/上界 + scorer 用)
//     - plan/engine/chain-composer.service.ts(cooldown 内不立"潜在新链",避免与 scenario 口径背离)
//     - plan/engine/priority-scorer.ts(goldenRange / urgency 算分)
// =============================================================

export interface DxTreatmentRule {
  categories: readonly PACTreatmentCategory[];
  cooldownDays: number;
  windowDays: number;
  urgencyDayThreshold: number;
  chainLabel: string;
  /// 全口/全牙弓性疾病(牙周 K05、正畸 K07):**召回侧忽略 dx 自带牙位**。
  ///   消费方:
  ///     - chain-composer:空牙位时显示"全口"(否则"未标注牙位")
  ///     - 召回 scenario(treatment-initiation-recall):排除闸 ⑤a 把信号强制视为无牙位 → category 级
  ///       排除;tooth-overlap 聚类归 'whole' cluster(sub_key=...@whole)。忽略 dx 偶带的单颗牙。
  ///   原因:全口病的诊断偶带某颗牙(录入习惯),但治疗可能在别的牙,单牙重叠判断会误召
  ///        (709686:K05@38 牙周治疗在 36 → 单牙不重叠 → 误召 perio_no_srp@38)。
  ///   一般跟 excludeIfEverTreated 同时设(全口长疗程病:忽略牙位 + 忽略时间方向)。
  wholeMouth?: boolean;
  /// 治疗"一次性发起、非按牙位"(全口长疗程/慢性,如正畸 K07、牙周 K05):
  /// 召回排除闸 ⑤a **忽略时间方向** —— 只要曾做过同类治疗即排除(不要求治疗晚于诊断)。
  /// 原因:这类病复诊会反复重记诊断,旧诊断之后没有"更晚的治疗"不代表没治 → 否则误召"未启动"。
  /// 按牙位的码(K02 龋/K04 根管/K08 缺牙等)不设此项 — 时间方向+牙位匹配才正确(新牙长龋该召)。
  excludeIfEverTreated?: boolean;
}

export const DiagnosisTreatmentMap = {
  // 诊断码(K0x)— 跟同临床类目的推荐码(*_RECOMMENDED)窗口/临界严格一致
  // W3 末扩 K00-K08 全覆盖(数据驱动:host DW 真实数据 244 万 EMR diag 频次扫描后落码,见 dw-data-source-issues #15)
  // 不漏码、不改诊断码(K06 病种保留为 K06,不归 K05);base 分按临床召回价值分级(K07 正畸 55 高 / K00 萌出 25 低)
  K00: { categories: ['surgical', 'prosthodontic', 'implant', 'orthodontic'], cooldownDays: 30, windowDays: 365, urgencyDayThreshold: 180, chainLabel: '先天/萌出处置' },
  K01: { categories: ['surgical'],               cooldownDays: 14, windowDays: 180, urgencyDayThreshold: 90,  chainLabel: '阻生牙拔除' },
  K02: { categories: ['restorative'],            cooldownDays: 14, windowDays: 90,  urgencyDayThreshold: 60,  chainLabel: '龋齿充填' },
  K03: { categories: ['restorative', 'prosthodontic', 'surgical'], cooldownDays: 14, windowDays: 90, urgencyDayThreshold: 60, chainLabel: '牙体修复' },
  K04: { categories: ['endodontic'],             cooldownDays: 14, windowDays: 60,  urgencyDayThreshold: 45,  chainLabel: '根管治疗' },
  // K05 categories=['periodontic'] 严格(W3 末撤回 C 修复)
  // 之前曾加 'preventive' 想让做过洁牙的患者不被召回,但临床上洁牙(¥260-350 预防) ≠ 龈上洁治术(¥600+ SRP)
  // host 数据已正确区分两者:subtype/category 字段不同。王辉就是"医生让做 SRP 但只做洁牙"典型患者 —
  // 应该被召回去促进真 SRP,而不是被算"已启动"放过。chain-composer 显示 S3 未进行也支持这判定
  K05: { categories: ['periodontic'],            cooldownDays: 30, windowDays: 120, urgencyDayThreshold: 90,  chainLabel: '牙周治疗', wholeMouth: true, excludeIfEverTreated: true },
  K06: { categories: ['periodontic', 'surgical'], cooldownDays: 14, windowDays: 120, urgencyDayThreshold: 60,  chainLabel: '牙龈/牙槽嵴处置' },
  // K07 wholeMouth:true — 正畸是全牙弓矫治,诊断 fact 偶带某颗牙(录入习惯)无临床意义。
  //   召回排除 ⑤a / 聚类按"全口/全牙弓"处理,忽略 dx 自带牙位(否则 dx@38 + 矫治记录@36 单牙不重叠 → 误召)。
  //   配合 excludeIfEverTreated(忽略时间方向):曾做过正畸即排除。两 flag 同治"全口长疗程"病。
  K07: { categories: ['orthodontic'],            cooldownDays: 30, windowDays: 365, urgencyDayThreshold: 180, chainLabel: '正畸矫治', wholeMouth: true, excludeIfEverTreated: true },
  K08: { categories: ['implant', 'prosthodontic'], cooldownDays: 30, windowDays: 180, urgencyDayThreshold: 120, chainLabel: '种植修复' },
  K09: { categories: ['surgical'],               cooldownDays: 14, windowDays: 90,  urgencyDayThreshold: 60,  chainLabel: '颌骨囊肿摘除' },
  // 推荐码 — 同临床类目继承 K0x 配置
  IMPLANT_RECOMMENDED:    { categories: ['implant'],       cooldownDays: 30, windowDays: 180, urgencyDayThreshold: 120, chainLabel: '种植修复' },
  CROWN_RECOMMENDED:      { categories: ['prosthodontic'], cooldownDays: 14, windowDays: 90,  urgencyDayThreshold: 60,  chainLabel: '冠修复' },
  FILLING_RECOMMENDED:    { categories: ['restorative'],   cooldownDays: 14, windowDays: 60,  urgencyDayThreshold: 45,  chainLabel: '龋齿充填' },
  SRP_RECOMMENDED:        { categories: ['periodontic'],   cooldownDays: 30, windowDays: 120, urgencyDayThreshold: 90,  chainLabel: '牙周治疗', wholeMouth: true },
  EXTRACTION_RECOMMENDED:    { categories: ['surgical'],      cooldownDays: 7,  windowDays: 30,  urgencyDayThreshold: 14,  chainLabel: '拔除' },
  ORTHO_CONSULT_RECOMMENDED: { categories: ['orthodontic'],   cooldownDays: 30, windowDays: 365, urgencyDayThreshold: 180, chainLabel: '正畸矫治', wholeMouth: true },
  ANNUAL_REVIEW_RECOMMENDED: { categories: ['periodontic'],   cooldownDays: 60, windowDays: 540, urgencyDayThreshold: 365, chainLabel: '年度复查' },
  RCT_RECOMMENDED:           { categories: ['endodontic'],    cooldownDays: 14, windowDays: 60,  urgencyDayThreshold: 45,  chainLabel: '根管治疗' },
  HARD_TISSUE_REPAIR_RECOMMENDED:     { categories: ['restorative', 'prosthodontic', 'surgical'], cooldownDays: 14, windowDays: 90, urgencyDayThreshold: 60, chainLabel: '牙体修复' },
  GUM_TREATMENT_RECOMMENDED:          { categories: ['periodontic', 'surgical'], cooldownDays: 14, windowDays: 120, urgencyDayThreshold: 60, chainLabel: '牙龈/牙槽嵴处置' },
  ERUPTION_INTERVENTION_RECOMMENDED:  { categories: ['surgical', 'orthodontic', 'prosthodontic', 'implant'], cooldownDays: 30, windowDays: 365, urgencyDayThreshold: 180, chainLabel: '先天/萌出处置' },
  JAW_CYST_REMOVAL_RECOMMENDED:       { categories: ['surgical'], cooldownDays: 14, windowDays: 90, urgencyDayThreshold: 60, chainLabel: '颌骨囊肿摘除' },
} as const satisfies Record<string, DxTreatmentRule>;

export type DiagnosisTreatmentCode = keyof typeof DiagnosisTreatmentMap;

/**
 * "算诊断但无修复必要"的诊断名(host 把它们映射到 K08 缺牙类,但牙还在、只是无功能)。
 * 临床:该拔除 / 观察,**不是种植/桥/义齿修复对象**。
 *
 * **单一真理源**:召回 scenario(剔除出召回池)和 chain-composer(不立"种植修复·发现机会"潜在链,
 *   改中性展示 + 建议拔除/观察)共用此表,避免两处口径分叉(canonical-fact-layer「单一收口」)。
 */
export const RESTORATION_INELIGIBLE_DX_NAMES = ['废用牙', '无功能牙'] as const;

/// 判断诊断 name_zh 是否属"无修复必要"(废用牙/无功能牙)
export function isRestorationIneligibleDxName(nameZh: string | null | undefined): boolean {
  return !!nameZh && (RESTORATION_INELIGIBLE_DX_NAMES as readonly string[]).includes(nameZh);
}

/// 查表(找不到返回 undefined — 调用方决定如何降级)
export function lookupDxTreatment(code: string): DxTreatmentRule | undefined {
  return (DiagnosisTreatmentMap as Record<string, DxTreatmentRule>)[code];
}

// =============================================================
// 召回"已解决"判定 —— 治疗家族 resolver(单一真理源)
//
// 背景:旧口径每个诊断码白名单只认 categories 里那几类治疗(K02 只认 restorative),
//   永远列不全 —— 龋齿做了"根管 / 垫底充填 / 贴面"都没被认作已治 → 误召(李梦维 1B 案例)。
//   差分测试(oracle vs 生产)实测:670 个按牙召回单元里,放宽到"任意局部结构治疗"后
//   18 个翻成已解决,人工核对全部合理(根管治龋 / 贴面修牙体 / 乳牙冠…)。
//
// 新口径:**同一颗牙诊断后做了任何【针对该病家族】的治疗 = 已处理**,不再纠结具体哪一类。
//   - 结构问题(K00/01/02/03/04/08/09)→ 任何【局部结构治疗】都算:
//       充填 / 嵌体 / 冠桥 / 种植 / 牙髓根管 / 外科 / 美学 / 儿牙
//     **刻意排除** periodontic(洁牙/刮治)/ orthodontic(矫治流程)/ preventive / review ——
//     这些是"跨病 / 流程"动作,会误销(洗牙≠补龋、刮治≠修缺牙,实测 17 例误翻转)。
//   - 牙周/牙龈(K05/K06)、正畸(K07)→ 维持各自 categories(已正确,牙周只认牙周/外科)。
//
// 形状:从「11 张码表」收成「结构家族一张表 + 牙周/正畸沿用各自」,稳定、可列全、0 误销。
// =============================================================

/// 局部结构治疗 = 结构类诊断的"已解决"判定集(不含 牙周/正畸/预防/复查流程)
export const STRUCTURAL_RESOLVER_CATEGORIES = [
  'restorative',
  'prosthodontic',
  'implant',
  'endodontic',
  'surgical',
  'cosmetic',
  'pediatric',
] as const satisfies readonly PACTreatmentCategory[];

/// 走"结构家族 resolver"的诊断码 / 推荐码(其余 K05/K06/K07 等沿用 rule.categories)
/// 结构性诊断/推荐码(按牙的牙体/牙髓/缺牙/外科问题)。
/// 用途①:resolverCategoriesFor 区分结构 vs 牙周/正畸。
/// 用途②:同牙位"取代"规则 —— 只有【结构码】能取代结构诊断;牙周(K05/K06)/正畸(K07)
///   是不同组织的并存病,不互相 moot(否则全口牙周诊断会误销一片龋齿)。
export const STRUCTURAL_DX_CODE_LIST = [
  'K00',
  'K01',
  'K02',
  'K03',
  'K04',
  'K08',
  'K09',
  'IMPLANT_RECOMMENDED',
  'CROWN_RECOMMENDED',
  'FILLING_RECOMMENDED',
  'EXTRACTION_RECOMMENDED',
  'RCT_RECOMMENDED',
  'HARD_TISSUE_REPAIR_RECOMMENDED',
  'ERUPTION_INTERVENTION_RECOMMENDED',
  'JAW_CYST_REMOVAL_RECOMMENDED',
] as const;
const STRUCTURAL_DX_CODES = new Set<string>([
  ...STRUCTURAL_DX_CODE_LIST,
]);

/**
 * 召回"已解决"判定:给定诊断/推荐码,返回"算作已处理"的治疗类别集合。
 *
 * ⚠️ 这是【判定已解决】用的(宽,家族级);跟【展示"未启动 X" + 触发预期】用的
 *    rule.categories(窄,主治疗)分开 —— 否则客服卡片会从"未启动 充填"变成一堆噪音。
 *
 * 单一真理源:召回 scenario 的 ⑤a 排除闸 + 牙位事实 oracle 对账 共用此函数,口径不漂移。
 */
export function resolverCategoriesFor(code: string): readonly PACTreatmentCategory[] {
  if (STRUCTURAL_DX_CODES.has(code)) return STRUCTURAL_RESOLVER_CATEGORIES;
  const rule = lookupDxTreatment(code);
  return rule ? rule.categories : [];
}

// =============================================================
// 治疗状态(PACTreatmentStatus)— treatment_record.content.status
// =============================================================

export const PACTreatmentStatuses = {
  planned: { nameZh: '计划中' },
  in_progress: { nameZh: '进行中' },
  completed: { nameZh: '已完成' },
  cancelled: { nameZh: '已取消' },
  failed: { nameZh: '治疗失败' },
} as const;
export type PACTreatmentStatus = keyof typeof PACTreatmentStatuses;
export const PACTreatmentStatusSchema = z.enum(
  Object.keys(PACTreatmentStatuses) as [
    PACTreatmentStatus,
    ...PACTreatmentStatus[],
  ],
);

// =============================================================
// 治疗链生命周期模型(TreatmentLifecycle + TreatmentMilestones)
// =============================================================
//
// 用途:chain-composer 的 5 阶段引擎读它判定 (patient × category) 当前 stage 和 closed/ongoing。
//
// **跟召回算法解耦**:scenario SQL 排除口径不读本字典(召回的是"未启动治疗"的患者,
// 按现有"诊断后无任何同类 actual"判定即可)。本字典纯展示用,让客服在详情页看清治疗链
// 真实进度。如果未来上"治疗链内召回"(种植已植入未修复等)再让那个新 scenario 读本字典。
//
// 5 阶段(对齐 design 文档 §0):
//   ① 发现治疗机会      = diagnosis / recommendation / image / EMR finding
//   ② 进入治疗链        = planned treatment / appointment / payment / 发现后接诊
//   ③ 治疗执行          = actual treatment(命中 milestone steps 数 >= minSteps)
//   ④ 术后与复查管理    = post-S3 review encounter / planned review / *_REVIEW_RECOMMENDED
//   ⑤ 治疗链闭环        = S3 全 milestone 满足 + S4 命中 + 无 refund 后置 + 无反弹诊断 + lifecycle 允许闭环
//
// **closed 条件比"做过 actual"严得多** — 路遥牙周(lifelong_maintenance)永远停在 stage=4。
// =============================================================

/// 治疗链生命周期类型 — 决定能否闭环 + 周期长度参考
export interface TreatmentLifecycle {
  /// 该类型能到的最高 stage(lifelong_maintenance 卡 4,永不 closed)
  maxStage: 4 | 5;
  /// 预期治疗周期(月)— S4→S5 判定时"X 个月内无新调整 → closed"用
  expectedSpanMonths?: number;
  /// 给 UI 提示 + 业务方调字典时用
  noteZh: string;
  /// W4 末:闭环额外要求同牙位有"冠保护"(prosthodontic actual:冠/桩核)
  /// 临床:根管后牙变脆,2-3 年内不戴冠 ~30% 折裂率 — 没戴冠不算真闭环
  /// chain-composer S5 闸口会读这个 flag 加判
  requiresCrownProtection?: boolean;
}

export const TreatmentLifecycles = {
  /// 一次治疗 + 一次复查即可闭环(充填 / 修复 / 拔除 / 美容 / 儿牙)
  one_shot:             { maxStage: 5, expectedSpanMonths: 3,  noteZh: '一次性治疗' },
  /// 多步治疗 + 复查即可闭环(种植 — implant 自带 crown_placement step,不需要额外要求)
  linear:               { maxStage: 5, expectedSpanMonths: 6,  noteZh: '多步线性治疗' },
  /// 多步治疗 + 必须冠保护(根管 — endodontic 完成后必须 prosthodontic 冠修复才算闭环)
  linear_then_crown:    { maxStage: 5, expectedSpanMonths: 6,  noteZh: '多步治疗 + 冠保护', requiresCrownProtection: true },
  /// 长周期治疗(正畸 2 年);S3 后 12+ 月平稳无新调整 → closed
  long_term:            { maxStage: 5, expectedSpanMonths: 24, noteZh: '长周期治疗' },
  /// 周期性治疗(预防/年度洁牙),每年 1 次为常态
  periodic:             { maxStage: 5, expectedSpanMonths: 12, noteZh: '周期性治疗' },
  /// 终身维护型(牙周炎慢性病)— 永远停留 stage=4,做完一次维护仍需周期复查
  lifelong_maintenance: { maxStage: 4,                          noteZh: '终身维护(永不闭环)' },
} as const satisfies Record<string, TreatmentLifecycle>;
export type TreatmentLifecycleKey = keyof typeof TreatmentLifecycles;

/// PAC 治疗 step 标准 enum(英文 snake_case)— **跟宿主无关**
///
/// 设计原则(W4 末):
///   - 字段名 + 内容都是 PAC 标准(host 字面值通过 yaml enum_mapping 翻译)
///   - 任何宿主接入:写 "开髓"/"拔髓"/"RCT-1"/"Endo Step 1" 都映射到 pulp_extirpation
///   - canonical 跟宿主语言/编码无关,只跟临床概念相关
///
/// 匹配规则(chain-composer.matchMilestoneSteps):
///   - 优先 treat_stages[]: actual.content.treat_stages 含 step 即命中(精确)
///   - fallback subtype 词根:host 没填 treat_stages 时,subtype.includes(host 词根)
export const PACTreatmentStep = {
  // 牙髓 / 根管
  pulp_extirpation:    '开髓 / 拔髓',
  canal_preparation:   '根管预备',
  canal_filling:       '根管充填',
  post_endo_filling:   '根管后充填',
  /// 活髓 / 部分牙髓切断术(vital pulpotomy / partial pulpotomy / 干髓术 — 等价术式)
  /// 临床:保留根髓,冠髓切除 + MTA/Biodentine 封盖,成年恒牙 80-90% 成功率(AAE/ESE 2019+ 认可为终末术式)
  /// 跟 canal_filling 并列作为 endodontic 的 terminalSteps —— 任一命中即 s3Reached
  /// 命中 pulpotomy 不要求冠保护(根髓活,牙不变脆)
  pulpotomy:           '牙髓切断术(活髓 / 部分 / 干髓)',
  // 种植
  implant_placement:   '种植体植入',
  abutment_placement:  '基台连接',
  crown_placement:     '种植上部修复(冠)',
  // 牙周
  supragingival_scaling: '龈上洁治',
  subgingival_scaling:   '龈下刮治',
  periodontal_maintenance: '牙周维护',
  // 修复
  composite_filling:   '树脂充填',
  inlay:               '嵌体',
  crown_restoration:   '冠修复',
  post_core:           '桩核',
  // 外科 / 拔除
  tooth_extraction:    '拔除',
  // 正畸
  bracket_placement:   '矫治器(粘附件)',
  retainer:            '保持器',
  ortho_adjustment:    '正畸加力调整',
  // 预防
  fluoride_application: '涂氟',
  pit_fissure_sealant:  '窝沟封闭',
  professional_cleaning: '洁牙',
  // 美学
  whitening:           '美白',
  veneer:              '贴面',
} as const;
export type PACTreatmentStepKey = keyof typeof PACTreatmentStep;

/// 治疗里程碑 — 每个 PAC category 的标准 step 列表
///
/// 匹配规则(chain-composer.matchMilestoneSteps,优先级从高到低):
///   ① actual.content.treat_stages[] 含本 step → 命中(精确,host 给了 stage 字段)
///   ② subtype.includes(legacySubtypeKeyword[step]) → 兜底匹配(host 没给 stage)
///
/// minSteps = 满足 stage=3 ongoing 的最少 step 数;到 minSteps 才算"治疗执行进入正轨"。
/// 注:种植 minSteps=2 — 单做植入未做上部修复 = stage=3 ongoing(在管),非闭环。
export interface TreatmentMilestone {
  /// 期望 step(PAC 标准 enum,按时间序);UI 显示 chain timeline 节点 label 也用它
  steps: readonly PACTreatmentStepKey[];
  /// 达到 stage=3 ongoing 的最少 step 数(closed 需要全 steps 满足)
  minSteps: number;
  /// 生命周期类型 → 查 TreatmentLifecycles 拿 maxStage / expectedSpanMonths
  lifecycle: TreatmentLifecycleKey;
  /// W4 末新增:**任一命中即视为治疗已完整收尾**(s3Reached=true,跳过 minSteps 检查)
  ///
  /// 用于支持"多路径终末"的临床现实:
  ///   endodontic:
  ///     Path A — 完整 RCT  (开髓 → 根管预备 → 根管充填) ✅ canal_filling 命中即收尾
  ///     Path B — 牙髓切断术(活髓 / 部分 / 干髓 — VPT 终末术式)✅ pulpotomy 命中即收尾
  ///
  /// 跟 requiresCrownProtection 配合:
  ///   命中 canal_filling → 仍需冠保护(linear_then_crown 规则)
  ///   命中 pulpotomy     → 不需冠保护(局部 override,chain-composer 处理)
  terminalSteps?: readonly PACTreatmentStepKey[];
}

export const TreatmentMilestones = {
  // terminalSteps: crown_placement(戴牙/上部修复)= 种植功能性完成 —— 临床上没有植体不可能戴冠,
  //   所以即使"种植体植入"步因数据缺失(无牙位 / 早于诊断 / subtype 不命中)没匹配上,戴冠即视为收尾。
  //   修复刘晓芳 47 案例:戴牙完成却因植入步丢失被误判 discovered ★(潜在新链)。
  //   单做植入未戴冠 → 仍 matched=1<minSteps=2 且非 terminal → s3 不达成(保持"待戴冠"语义)。
  implant:       { steps: ['implant_placement', 'crown_placement'],                        minSteps: 2, lifecycle: 'linear', terminalSteps: ['crown_placement'] },
  // endodontic 3 步 + minSteps=2(开髓 + 根充至少);patient 真做了开髓+根备+根充才算完整
  // lifecycle=linear_then_crown:闭环额外要求 prosthodontic 冠保护(临床:根管后不戴冠折裂率 ~30%)
  endodontic:    { steps: ['pulp_extirpation', 'canal_preparation', 'canal_filling', 'pulpotomy'], minSteps: 2, lifecycle: 'linear_then_crown', terminalSteps: ['canal_filling', 'pulpotomy'] },
  orthodontic:   { steps: ['bracket_placement', 'retainer'],                              minSteps: 1, lifecycle: 'long_term' },
  periodontic:   { steps: ['supragingival_scaling', 'subgingival_scaling', 'periodontal_maintenance'], minSteps: 1, lifecycle: 'lifelong_maintenance' },
  restorative:   { steps: ['composite_filling', 'inlay'],                                 minSteps: 1, lifecycle: 'one_shot' },
  prosthodontic: { steps: ['crown_restoration', 'post_core'],                             minSteps: 1, lifecycle: 'one_shot' },
  surgical:      { steps: ['tooth_extraction'],                                           minSteps: 1, lifecycle: 'one_shot' },
  preventive:    { steps: ['professional_cleaning', 'fluoride_application', 'pit_fissure_sealant'], minSteps: 1, lifecycle: 'periodic' },
  cosmetic:      { steps: ['whitening', 'veneer'],                                        minSteps: 1, lifecycle: 'one_shot' },
  pediatric:     { steps: [],                                                              minSteps: 1, lifecycle: 'one_shot' },
} as const satisfies Partial<Record<PACTreatmentCategory, TreatmentMilestone>>;

/// chain-composer fallback 用:host 没填 treat_stages 时,subtype 词根匹配
/// 这一段是 jvs-dw 特化的 fallback;别的宿主可以另写一份(进 yaml/per-host)
/// 真上多宿主时把这个挪到 per-host yaml(短期 hardcode)
export const LegacyStepSubtypeKeywords: Partial<Record<PACTreatmentStepKey, readonly string[]>> = {
  pulp_extirpation: ['开髓', '拔髓'],
  canal_preparation: ['根备', '根管预备'],
  // canal_filling = RCT 终末步。除细分"根充/根管充填",再纳入**整根管术式全称/缩写**:
  //   host 结算项常写笼统名(根管治疗 / 根管再治疗 / RCT / 根管治疗+冠修复…),不写细分步骤 →
  //   旧版匹配 0/4 步骤,链误判"未做根管"(实际已做)。这些全称词即"根管已完成"的等价信号,归终末步。
  //   ⚠️ 仍只匹配 subtype 不含细分 stages 的记录(matchMilestoneSteps:有 stages 则以 stages 为准),
  //      子步骤(开髓/根备/根充)语义完整保留;"根管预备/根充"等仍各归其步,不受影响。
  canal_filling: ['根充', '根管充填', '根管治疗', '根管再治疗', '根管二次治疗', 'RCT'],
  // pulpotomy 类:活髓切断 / 部分活髓切断 / 部分牙髓切断 / 冠髓切断 / 干髓 / 盖髓 / 牙髓血运重建
  // 这些都是"保留根髓"的终末术式(VPT — vital pulp therapy),临床等价
  pulpotomy: ['活髓切断', '部分活髓切断', '部分牙髓切断', '冠髓切断', '干髓', '盖髓', '牙髓血运重建', '活髓保存'],
  // 种植一/二/三期都是"外科植入阶段"(一期植入 → 二期暴露/愈合帽 → 三期),归 implant_placement;
  //   不能进 crown_placement,否则"只做二期没戴冠"会被 terminalSteps 误判为种植完成。
  implant_placement: ['种植体植入', '种植钉植入', '种植手术', '种植一期', '种植二期', '种植三期', '简单种植', '复杂种植', '即刻种植', '即拔即种', '延期种植', '拔除后种植'],
  // crown_placement = 真正"戴上冠/上部修复完成"的终末信号(见 TreatmentMilestones.implant.terminalSteps)
  //   除"种植上部修复/冠修复/戴牙",再纳入笼统全称 **种植修复**(host 结算项最常用,= 种植上部修复完成)、
  //   种植冠加瓷(烤瓷冠)。同 endodontic 的"根管治疗"全称兜底:不写细分步骤的整术式记录也能认出"已修复"。
  //   注:"种植上部修复"含"修复"但不含"种植修复"子串,两者各自独立匹配,不冲突。
  crown_placement: ['种植上部修复', '种植冠修复', '种植戴牙', '种植修复', '种植冠加瓷'],
  supragingival_scaling: ['洁治', '洁牙', '洗牙'],
  // 笼统全称"牙周基础治疗/牙周系统治疗"(host 结算项常用,= 已做 SRP 系统治疗)归 SRP 步,
  //   否则牙周链误判"未做"。注:洁牙 vs SRP 的召回区分在 scenario 侧(SRP_RECOMMENDED),展示层不受影响。
  subgingival_scaling: ['刮治', '龈下', '牙周基础治疗', '牙周系统治疗'],
  periodontal_maintenance: ['维护'],
  composite_filling: ['充填'],
  inlay: ['嵌体'],
  // crown_restoration:除"冠",纳入"戴牙"(冠桥/义齿戴入完成 = 修复终末)。
  crown_restoration: ['冠', '戴牙'],
  post_core: ['桩核'],
  tooth_extraction: ['拔除', '拔牙'],
  // bracket_placement:除具体器械(矫治器/托槽/附件),纳入笼统全称"正畸/正畸治疗/矫治"
  //   (host 结算项最常写"正畸"——做正畸必然已戴矫治器,= 已进入正畸执行)。
  bracket_placement: ['矫治器', '附件', '托槽', '正畸', '正畸治疗', '矫治'],
  retainer: ['保持器'],
  ortho_adjustment: ['加力', '调整'],
  fluoride_application: ['涂氟'],
  pit_fissure_sealant: ['封闭'],
  professional_cleaning: ['洁牙'],
  whitening: ['美白'],
  veneer: ['贴面'],
};

/// 查表(category 未定义 milestone → undefined,chain-composer 默认 minSteps=1 one_shot)
export function lookupTreatmentMilestone(
  category: string,
): TreatmentMilestone | undefined {
  return (TreatmentMilestones as Record<string, TreatmentMilestone>)[category];
}

export function lookupTreatmentLifecycle(
  key: TreatmentLifecycleKey,
): TreatmentLifecycle {
  return TreatmentLifecycles[key];
}

// =============================================================
// 牙位(FDIToothPosition)— FDI 国际标准 11-48(恒牙)+ 51-85(乳牙)
// =============================================================

const PERMANENT = [
  '11', '12', '13', '14', '15', '16', '17', '18',
  '21', '22', '23', '24', '25', '26', '27', '28',
  '31', '32', '33', '34', '35', '36', '37', '38',
  '41', '42', '43', '44', '45', '46', '47', '48',
];
const DECIDUOUS = [
  '51', '52', '53', '54', '55',
  '61', '62', '63', '64', '65',
  '71', '72', '73', '74', '75',
  '81', '82', '83', '84', '85',
];

/// 全部合法 FDI 牙位(永久牙 32 + 乳牙 20 = 52)
export const FDI_TOOTH_POSITIONS = [...PERMANENT, ...DECIDUOUS] as const;
export type FDIToothPosition = (typeof FDI_TOOTH_POSITIONS)[number];
export const FDIToothPositionSchema = z.enum(
  FDI_TOOTH_POSITIONS as unknown as [FDIToothPosition, ...FDIToothPosition[]],
);

// =============================================================
// 影像模态(PACImageModality)— image_record.content.modality
// =============================================================

export const PACImageModalities = {
  pa: { nameZh: '根尖片' },
  bw: { nameZh: '咬翼片' },
  pano: { nameZh: '曲面断层' },
  cbct: { nameZh: 'CBCT 锥形束 CT' },
  intraoral_photo: { nameZh: '口内照片' },
  scan: { nameZh: '口扫模型' },
  other: { nameZh: '其他' },
} as const;
export type PACImageModality = keyof typeof PACImageModalities;
export const PACImageModalitySchema = z.enum(
  Object.keys(PACImageModalities) as [PACImageModality, ...PACImageModality[]],
);

// =============================================================
// Recommendation 提取来源(content.extractedBy)
// =============================================================

export const RecommendationExtractedBy = {
  rule: 'rule', // 关键词规则(兜底)
  llm: 'llm', // Layer C LLM 抽取
  doctor_input: 'doctor_input', // 医生显式录入(host 结构化字段)
} as const;
export type RecommendationExtractedBy =
  (typeof RecommendationExtractedBy)[keyof typeof RecommendationExtractedBy];
export const RecommendationExtractedBySchema = z.enum([
  'rule',
  'llm',
  'doctor_input',
]);

// =============================================================
// 工具:运行时检查 code 是否合法(yaml 校验 / CI 防漂移用)
// =============================================================

export function isPACDiagnosisCode(v: unknown): v is PACDiagnosisCode {
  return typeof v === 'string' && v in PACDiagnosisCodes;
}

export function isPACTreatmentCategory(v: unknown): v is PACTreatmentCategory {
  return typeof v === 'string' && v in PACTreatmentCategories;
}

export function isFDIToothPosition(v: unknown): v is FDIToothPosition {
  return (
    typeof v === 'string' &&
    (FDI_TOOTH_POSITIONS as readonly string[]).includes(v)
  );
}

// =============================================================
// 工具:code → 中文名(单一真理源,前后端共用)
//   - 后端拼"自然语言句子"(plan reason / persona description)时用,根上不漏 enum code
//   - 前端展示"字段型 enum"(category / status / code 列)时也用,不必接口冗余塞 xxName
//   - 查不到回退原值(防漂移:新码忘登记时至少显示 code,不报错)
// =============================================================

/** 诊断码 / 推荐码 / 业务码 → 中文名(K08 → 牙列丢失 / 缺牙) */
export function diagnosisCodeNameZh(code: string): string {
  return (
    (PACDiagnosisCodes as Record<string, { nameZh: string }>)[code]?.nameZh ??
    code
  );
}

/** 治疗类别 → 中文名(implant → 种植) */
export function treatmentCategoryNameZh(category: string): string {
  return (
    (PACTreatmentCategories as Record<string, { nameZh: string }>)[category]
      ?.nameZh ?? category
  );
}

/**
 * 乳牙判定(单牙位 token)— FDI 51-85(`^[5-8][1-5]$`)+ 宿主象限记法 1A-4E(`^[1-4][A-E]$`)。
 * 恒牙 FDI 11-48(象限 1-4 + 数字 1-8)不匹配。前后端 / 召回 SQL 同口径(单一真理源)。
 */
export function isDeciduousTooth(token: string): boolean {
  const t = token.trim();
  return /^[5-8][1-5]$/.test(t) || /^[1-4][A-Ea-e]$/.test(t);
}

/** 牙位字符串是否"全为乳牙"(非空且每颗都是乳牙)。空串 → false。 */
export function isAllDeciduous(toothPosition: string | null | undefined): boolean {
  const teeth = (toothPosition ?? '')
    .split(/[;,]/)
    .map((x) => x.trim())
    .filter(Boolean);
  return teeth.length > 0 && teeth.every(isDeciduousTooth);
}

/**
 * 治疗类别 → 中文名,**按牙位裁剪**:乳牙的 restorative 只做直接充填(嵌体是恒牙间接修复体)
 * → 返回「充填」而非「充填 / 嵌体」。其余类别 / 恒牙不变。前后端共用,保证文案一致。
 */
export function treatmentCategoryNameZhForTeeth(
  category: string,
  toothPosition: string | null | undefined,
): string {
  if (category === 'restorative' && isAllDeciduous(toothPosition)) return '充填';
  return treatmentCategoryNameZh(category);
}

// =============================================================
// scenario 子规则 / 触发类型 → 中文标签(前后端共用)
// =============================================================

/**
 * scenario × sub_key 复合 key → 子规则中文标签。
 * 跟 scenario plugin 内 cfg.label 同一份语义,提取到这里作单一源,
 * 前端用 subLabelZh(scenarioKey, subKey) 翻译,scenario 内 cfg 改成 reference。
 */
export const PACScenarioSubLabels: Record<string, string> = {
  // treatment_initiation_recall 9 子规则(W3 末 K00-K09 全覆盖)— 跟 scenario plugin SUB_SCENARIOS.label 一一对齐
  'treatment_initiation_recall.missing_tooth': '缺失牙未启动修复',                // K08
  'treatment_initiation_recall.ortho_no_consult': '错颌畸形未启动正畸',           // K07
  'treatment_initiation_recall.endo_no_rct': '牙髓炎 / 根尖周炎未做根管',         // K04
  'treatment_initiation_recall.perio_no_srp': '牙周炎未做基础治疗',               // K05
  'treatment_initiation_recall.caries_no_filling': '龋齿未做充填',                // K02
  'treatment_initiation_recall.hard_tissue_damage': '牙体损伤未修复',             // K03
  'treatment_initiation_recall.gum_alveolar_lesion': '牙龈 / 牙槽嵴疾患未处置',   // K06
  'treatment_initiation_recall.impacted_tooth': '阻生牙未拔除',                   // K01
  'treatment_initiation_recall.jaw_cyst': '颌骨囊肿未处理',                       // K09
  'treatment_initiation_recall.development_eruption': '牙发育 / 萌出异常未处置',  // K00
};

export function subLabelZh(
  scenarioKey: string,
  subKey: string | null | undefined,
): string {
  if (!subKey) return scenarioKey;
  return PACScenarioSubLabels[`${scenarioKey}.${subKey}`] ?? subKey;
}

/** 触发信号类型 → 中文标签(用于"诊断 / 医生建议 / 影像所见 / ..." 显示)*/
export const PACTriggerTypeLabels: Record<string, string> = {
  diagnosis: '诊断',
  recommendation: '医生建议',
  image_finding: '影像所见',
  emr_signal: '病历信号',
  visit_gap: '到诊间隔',
  other: '其他',
};

export function triggerTypeLabelZh(type: string): string {
  return PACTriggerTypeLabels[type] ?? type;
}

/**
 * PAC 立柱标准:获客渠道(初诊来源,A.2.1)— 单一收口。
 * host 各自的渠道叫法经 assembler enum_mapping 归一到这套标准 code(多宿主共用)。
 * 二级渠道(sub_category)host 特异,不在此枚举(原值透传 + 展示)。
 */
export const PACAcquisitionChannels: Record<string, string> = {
  walk_in: '走入',
  word_of_mouth: '口碑客户',
  group_sales: '集团销售渠道',
  group_marketing: '集团营销渠道',
  regional_marketing: '地区营销渠道',
  ecommerce: '电商平台',
  social_media: '自媒体网络',
  employee: '内部员工',
  other: '其他',
};

export function acquisitionChannelLabelZh(code: string | null | undefined): string {
  return (code && PACAcquisitionChannels[code]) || '未知';
}
