/**
 * PAC Canonical Codes — 集团统一字典(单一真理源)
 *
 * **定位**:所有 fact.content 里出现的 enum 值,必须 ∈ 本文件定义的闭集。
 * 宿主侧任意码(`A01` / `诊002` / `龋齿`)→ Layer A yaml `enum_mapping` 翻译到此处。
 *
 * **维护流程**:新增码走 PR,至少 1 牙医 + 1 业务方 + 1 PAC 工程 review。
 * 必填:含义 / 临床触发场景 / 算法用法 / 中文名。
 *
 * 详见 `apps/pac-docs/content/docs/architecture/data-ingestion.mdx` §三。
 */
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: '建议拔除' },                       // 跨病种拔除动作 → 独立 extraction_recommended 场景(不绑 K 码)
  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;

/// 检查所见(emr_record.exam_findings 的牙位 message)里"缺牙但无修复指征"的说法。
/// 王敏 BJ0U017412 牙26:诊断只写"牙列缺损",检查里写"缺失,缺牙间隙已关闭"→ 间隙没了无需修复。
/// ⚠️ 必须与"缺失/缺牙"在同一牙位 message 共现 —— 单独"间隙关闭"绝大多数是正畸语境
/// (矫正过程关闭间隙=治疗动作),会误排;共现限定后全库 104 患者(2026-06 实测)。
/// 「间隙不足」(李强 BJ0U017401 牙36;46:缺牙十年邻牙倾倒,空间不够直接修复 → 需先正畸)
/// 也归此类:对 K08 缺牙召回效果一致(不该推"快去种植修复")。共现"缺牙"护栏已挡掉正畸
/// 拥挤语境(全库 844 段「间隙不足」仅 246 段共现缺牙,均为真·缺牙无空间)。
/// 回访记录里"外院已治疗"信号(李姝妤②:回访结果"已在外院矫正中")。
/// 患者去外院做了治疗 → 患者级排除该诊断召回(回访日须晚于诊断,同治疗排除的时间方向;
/// 新诊断更晚 → 重新可召)。信号在 patient_return_visits.result(展示表),算法只读不写,
/// 不影响已治疗事实。**只 match result 字段**(follow_content 是营销标签/任务主题如
/// "种植潜客/洁牙",拼进来会误命中)。
///
/// 外院同义词覆盖:外院/他院/别院/别的医院/其他医院/换了医院(不含泛"医院",避免误伤本院)。
/// POS:院字与治疗词【紧邻】(双向,≤3 字)—— "外院治疗/已在别院做了/在别的医院矫正了";
///   只认实质治疗词(治疗/种植/矫正/修复/根管/补牙/做了/做过),不含复查/洁牙/检查(非治疗)。
/// NEG:排意图未去(建议/打算/考虑/会去/要去/咨询了/不考虑…院);标点边界 [^，；。] 保护多子句真阳。
/// 纯"已治疗"不带院字(111 条)不收 —— 可能指本院,本院治疗本应由 treatment_record 事实处理。
/// 数据验证(2026-06 全库):POS∧¬NEG 命中 977 患者。
export const EXTERNAL_TREATMENT_VISIT_POS_RE =
  '(外院|他院|别院|别的医院|其他医院|换了医院)[^，；。]{0,3}(治疗|种植|矫正|矫治|修复|根管|补牙|做了|做过)|(治疗|种植|矫正|矫治|修复|根管|补牙|做了|做过)[^，；。]{0,3}(外院|他院|别院|别的医院|其他医院)';
export const EXTERNAL_TREATMENT_VISIT_NEG_RE =
  '(建议|打算|考虑|可能|拟|会去|要去|去咨询|去看|咨询了|不考虑)[^，；。]{0,8}(外院|他院|别院|别的?医院|其他医院)';

/// 宿主 std_code 错标纠偏:K00=牙发育/萌出障碍(含先天缺牙),但宿主常把【后天缺失】的
/// "牙齿缺少/缺失/缺牙"也错标成 K00(全库 1044 例)→ 冒出"萌出异常"误召(关平 BJ0U007377 牙25:
/// 残根拔除后缺失,宿主标 K00 → 误召萌出处置)。区分符=「先天」:有先天 → 真 K00(发育障碍),
/// 无先天的缺牙义 → 后天缺失 = K08。萌出/多生牙等其它 K00 不动。
export function reconcileDiagnosisCode(code: string | null, nameZh: string | null): string | null {
  if (code === 'K00' && nameZh && /缺(牙|失|少)/.test(nameZh) && !/先天/.test(nameZh)) {
    return 'K08';
  }
  return code;
}

/// 治疗记录 subtype 里"患者拒绝该类治疗"的说法(余奕铭 BJ0F022277:planned 治疗
/// "正畸取资料,患者无意愿"/"洁牙,患者无意愿"→ 患者当场拒绝该 category)。
/// 语义=对【本次诊断】排除该治疗类目(同"已启动治疗"对称,靠时间方向;新诊断不受影响)。
/// 全库 treatment_record subtype 命中 1225 患者(正畸434/牙周267/种植103…)。
export const TREATMENT_REFUSAL_SUBTYPE_PATTERNS = ['无意愿', '不愿', '拒绝'] as const;

/// 影像拒绝(拒绝拍片 / 拒绝拍全景片 / 拒绝影像…)= 拒绝【拍片】≠ 拒绝【治疗】。
/// 判定 refusal 前先把这类影像拒绝短语抹掉再匹配 —— 否则"拒绝拍片但做了洁治"会被 `拒绝` 误当成
/// "拒绝该类治疗"从而错误压制召回(2026-07 数据核查:review 235 无害 + perio/种植/正畸等约 30 人真误伤)。
/// `拒绝` + ≤6 个非标点字符 + 影像名词(片/影像/X光/CBCT/全景/放射/曲面断层)。
export const TREATMENT_REFUSAL_IMAGING_EXCLUDE_RE =
  '拒绝[^，。；;、]{0,6}(片|影像|X光|CBCT|全景|放射|曲面断层)';

export const NO_RESTORATION_GAP_EXAM_PATTERNS = [
  '间隙关闭',
  '间隙已关闭',
  '无修复间隙',
  '间隙不足',
] 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 RecommendationExtractedBySchema = z.enum([
  'rule',
  'llm',
  'doctor_input',
]);

// =============================================================
// 工具: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);
}

// =============================================================
// 建议治疗的年龄适配(源头:诊断 → 建议治疗的**排序**;不改排除闸、不改是否召回)
// =============================================================

/**
 * 「活动义齿优先」年龄线 —— **> 该年龄**时,缺牙修复的建议顺序把活动义齿提到种植之前。
 *
 * 临床口径(2026-07 与业务方对齐):
 *   - 70-80 岁**不是种植禁忌** —— 能不能种要看骨量骨质、全身状况与患者意愿,由医生评估;
 *   - 但高龄群体整体上创伤更小、周期更短的活动义齿更稳妥,**推荐时应先讲义齿**;
 *   - 两者**并行提供**,不替患者做决定,也不替医生下禁忌结论。
 */
export const DENTURE_FIRST_AGE = 70;

/**
 * 「种植降位」年龄线 —— **≤ 该年龄**时把种植挪到建议序末位。
 *
 * 临床口径:种植体是骨结合的刚性锚,**不随颌骨生长移动** —— 颌骨发育完成前(女约 17-18、
 *   男约 18-20)植入,会随邻牙继续萌出而相对埋没、咬合紊乱,是公认的相对禁忌。
 *
 * ⭐ 边界必须是 **≤18**,与全仓另外三处种植年龄闸同口径 —— 差一岁就会自相矛盾:
 *     contraindication.feature.ts   `age <= 18`  → 打「种植禁忌」
 *     potential-treatment.feature.ts `age > 18`  → 才产「潜在种植」
 *     fact-block.ts noXray           `age <= 18` → 话术禁提拍片
 *   2026-07-27 生产实测:恰好 18 岁的患者有 1,299 条 plan,其中 140 条 focusCategory=implant。
 *   若本条按 <18 判,这 140 条会在**同一屏**上出现「目标 · 种植」+ 画像「种植禁忌」。
 *
 * 处理方式与 [[DENTURE_FIRST_AGE]] 一致:**只挪位不删** —— PAC 不替医生下禁忌结论,
 *   但也绝不把「目标 · 种植」摆到未成年人头上(2026-07 线上实测 1462 条,其中 731 条 <12 岁)。
 */
export const IMPLANT_LAST_AGE = 18;

/**
 * 「早期矫治」措辞年龄线 —— **≤ 该年龄**时 orthodontic 的展示名说「早期矫治」。
 *
 * 临床口径:早矫(一期)在替牙期 / 混合牙列做,约 6-12 岁,目标是干预颌骨发育、不良习惯
 *   与间隙管理;12 岁后恒牙列已成,是常规(二期)正畸,目标是排齐与咬合精调。
 *
 * ⚠️ 只改**措辞**不改类目 —— 两者同属 orthodontic,排除闸不受影响。
 * ⚠️ 年龄只是**默认推定**,推不翻"早矫已在外院做完"这类病历事实(那需要外院治疗史通道,
 *   本函数够不着);措辞层面按年龄给最可能的说法,准确度由上游事实兜。
 */
export const EARLY_ORTHO_MAX_AGE = 12;

/**
 * K00 主类目细分(**同码内重排,不改诊断码**)。
 *
 * 背景(2026-07 线上反馈 陈芃霖 BJ0A094257):K00「牙发育和萌出障碍」是个大口袋 ——
 *   乳牙滞留(该掉没掉,要拔)/ 乳牙早失(已经掉了,要管间隙)/ 先天缺失 / 釉质发育不全 /
 *   萌出障碍 语义完全不同,却共用一份固定类目序 `[surgical, ...]` → focusCategory 恒为
 *   surgical,给「乳牙早失」也打「目标 · 外科」(没牙可拔)。线上约 2800 条这样的错标。
 *
 * ⚠️ **诊断码不动** —— K00 还是 K00(不像 302 行那条"后天缺牙 → K08"是真的标错了码);
 *   这里只是同一个 K00 内部按诊断词把最贴切的类目提到首位,其余保持原相对顺序。
 * ⚠️ 与 [[recommendedCategoriesForAge]] 同边界:只挪位,返回集合恒等于入参集合,排除闸不受影响。
 *
 * 匹配顺序敏感 —— 自上而下首个命中生效(「乳牙早失」须先于泛化的「缺失」)。
 * 未命中(含 name_zh 为空的历史数据)→ 原样返回,行为与修复前一致。
 */
export const K00_LEAD_CATEGORY_RULES: ReadonlyArray<{
  pattern: RegExp;
  lead: string;
  why: string;
}> = [
  // 该掉没掉 / 多余牙 / 该掉在掉 —— 有实体牙要处理,外科正确(线上占比最大,现状即对)
  { pattern: /滞留|多生|松动/, lead: 'surgical', why: '有实体牙待拔' },
  // 已经掉了 —— 没牙可拔,临床是间隙保持 / 早期矫治
  { pattern: /早失/, lead: 'orthodontic', why: '牙已缺失,管间隙' },
  // 萌不出来 / 长歪了 —— 助萌、导萌、排齐
  { pattern: /萌出|迟萌|阻萌|埋伏|异位/, lead: 'orthodontic', why: '萌出障碍待导萌' },
  // 先天没长 —— 修复方向(未成年由年龄闸把种植压后,见 recommendedCategoriesForAge)
  { pattern: /先天缺|缺失|缺牙/, lead: 'prosthodontic', why: '先天缺牙待修复' },
  // 形态 / 色泽 / 结构异常 —— 冠或贴面类修复
  { pattern: /釉质|氟斑|四环素|过小牙|畸形牙|发育不全|发育不良/, lead: 'prosthodontic', why: '牙体形态色泽异常待修复' },
];

/**
 * 诊断词细分建议类目(目前只作用于 K00)—— 排在年龄适配**之前**的一道语义重排。
 *
 * 顺序:`refineCategoriesForDiagnosis`(临床语义)→ `recommendedCategoriesForAge`(年龄)。
 *   前者定"这个病该往哪个方向治",后者定"这个岁数该先讲哪个"。两道都只挪位。
 *
 * @param code       诊断码(非 K00 一律原样返回)
 * @param nameZh     诊断原文词(DW diag message,如「乳牙早失」);空 → 原样返回
 * @param categories 该诊断的候选治疗类目
 */
export function refineCategoriesForDiagnosis(
  code: string | null | undefined,
  nameZh: string | null | undefined,
  categories: readonly string[],
): readonly string[] {
  if (code !== 'K00') return categories;
  const name = (nameZh ?? '').trim();
  if (!name) return categories;
  const hit = K00_LEAD_CATEGORY_RULES.find((r) => r.pattern.test(name));
  if (!hit || !categories.includes(hit.lead)) return categories;
  return [hit.lead, ...categories.filter((c) => c !== hit.lead)];
}

/**
 * 诊断 → 建议治疗类目(**按年龄排序**)—— 建议侧的单一真理源。
 *
 * ⚠️ 边界:只调整**顺序**,不增删类目 —— 返回集合恒等于入参集合。
 *   排除闸(`DiagnosisTreatmentMap[].categories`:做过其中任一即算已解决)完全不受影响。
 *
 * 两条规则(互斥 —— 年龄不可能同时 >70 与 <18):
 *   ① 候选同时含 implant + prosthodontic(缺牙 K08 这类"种植或义齿"并行可选)时,
 *      年龄 > DENTURE_FIRST_AGE → prosthodontic 提到 implant 之前(义齿在前,种植并行保留);
 *   ② 年龄 ≤ IMPLANT_LAST_AGE(颌骨未发育完成)→ implant 挪到**末位**(仍保留,不删)。
 *   其余情况原样返回。
 *
 * @param categories 该诊断的候选治疗类目
 * @param age        患者年龄;null/undefined(无生日)→ 不排序,原样返回
 */
export function recommendedCategoriesForAge(
  categories: readonly string[],
  age: number | null | undefined,
): readonly string[] {
  if (age == null) return categories;
  // ② 未成年:种植降到末位(与 ① 互斥,先判 —— 低龄不可能同时触发义齿前置)
  if (age <= IMPLANT_LAST_AGE && categories.includes('implant') && categories.length > 1) {
    return [...categories.filter((c) => c !== 'implant'), 'implant'];
  }
  // ① 高龄:活动义齿前置
  const dentureFirst =
    age > DENTURE_FIRST_AGE &&
    categories.includes('implant') &&
    categories.includes('prosthodontic');
  if (!dentureFirst) return categories;
  // 只挪位:prosthodontic 提到最前,其余保持原相对顺序(种植仍在,作并行选项)
  return [
    'prosthodontic',
    ...categories.filter((c) => c !== 'prosthodontic'),
  ];
}

/**
 * 建议治疗的中文标签(年龄 + 牙位双适配)—— 前后端共用,保证目标标签 / 理由 / 话术一致。
 * 高龄的 prosthodontic 明确说「活动义齿」(而非泛化的"修复 / 冠桥" —— 冠桥需邻牙条件,
 * 高龄同样受限,说清是活动义齿更贴合实际推荐)。
 * 低龄的 orthodontic 明确说「早期矫治」(替牙期做的是一期干预,不是排齐恒牙列)。
 */
export function treatmentCategoryNameZhFor(
  category: string,
  toothPosition: string | null | undefined,
  age: number | null | undefined,
): string {
  if (category === 'prosthodontic' && age != null && age > DENTURE_FIRST_AGE) {
    return '活动义齿';
  }
  if (category === 'orthodontic' && age != null && age <= EARLY_ORTHO_MAX_AGE) {
    return '早期矫治';
  }
  return treatmentCategoryNameZhForTeeth(category, toothPosition);
}

// =============================================================
// 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
  'treatment_initiation_recall.extraction_recommended': '牙未拔除',               // 建议拔除(跨病种,不绑 K 码)
};

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: '影像AI',
  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]) || '未知';
}
