import { z } from 'zod';

/**
 * 临床信号(clinical_signals)—— 从病历自由文本抽出的结构化旁注
 *
 * 【为什么写在源 fact 的 content 里,而不是新建一个衍生 fact 类型】
 *   FactType 的设计纪律(enums/index.ts §FactType)明确写着:
 *     「fact 只装"源事件",不做衍生 fact 派生 — 衍生信息留在源 fact 的 content JSONB 里」
 *   信号是对**同一条病历**的结构化解读,不是新事件 —— 它跟着源 fact 的生命周期走:
 *   病历被订正 → transaction 重放 → 信号自动重抽;病历作废 → status 变更,信号随之失效。
 *   若另立 fact 类型,就要自己维护一套「源 fact 变了信号怎么办」,凭空造出一致性问题。
 *
 * 【为什么 valid_until 在抽取时就算好】
 *   时效规则(妊娠 280 天 / 哺乳 180 天 / 放疗终身)只活在 clinical-signals.yaml 一处。
 *   下游两个消费者各自只做 `valid_until == null || valid_until > now` 这一个比较:
 *     - 画像(TS):contraindication.feature.ts
 *     - 召回闸门(SQL,预留):一行 NOT EXISTS,不必在 SQL 里重写一遍时效规则
 *   逻辑不分叉 —— 与 ⑤g 未来回访闸「把判断下沉到数据」是同一个手法。
 */

/** 信号影响的治疗域。按**病因**建模、按治疗域映射 —— 一个抗凝药同时禁种植/拔牙/牙周手术。 */
export const TreatmentDomain = {
  IMPLANT: 'implant', // 种植
  SURGERY: 'surgery', // 外科(拔牙 / 翻瓣 / 植骨)
  ANESTHESIA: 'anesthesia', // 局部麻醉
  ORTHODONTIC: 'orthodontic', // 正畸
  XRAY: 'xray', // 放射检查
} as const;
export type TreatmentDomain = (typeof TreatmentDomain)[keyof typeof TreatmentDomain];
export const TreatmentDomainSchema = z.enum(['implant', 'surgery', 'anesthesia', 'orthodontic', 'xray']);

/**
 * 信号严重度 —— 决定下游怎么用,是整个设计的收束点。
 *   absolute 绝对禁忌:打禁忌标签;将来接召回闸门挡的就是这一档
 *   relative 相对禁忌:打标签 + 话术警示,**不挡召回**(能不能做由医生按身体条件评估)
 *   check    核查项  :只提示「预约前确认 X」,不叫禁忌 —— 定量条件无数据的一律落这档
 */
export const SignalSeverity = {
  ABSOLUTE: 'absolute',
  RELATIVE: 'relative',
  CHECK: 'check',
} as const;
export type SignalSeverity = (typeof SignalSeverity)[keyof typeof SignalSeverity];
export const SignalSeveritySchema = z.enum(['absolute', 'relative', 'check']);

/**
 * 单条抽取出的信号。写进 `patient_facts.content.clinical_signals[]`。
 *
 * ⚠️ `negated` 是三态,不是布尔:
 *   false — 阳性(患者有)
 *   true  — 明确否定(「否认药物过敏史」)。**阴性证据也存** —— 它能区分「没记录」和
 *           「医生问过、患者否认」,话术里可以说「上次医生记录您没有药物过敏」;
 *           也是防止下游某个消费者把「没查到」误当「没有」的护栏。
 *   null  — 不可判。源数据本身没有结论(医生没删模板里的「否认/自述有」斜杠,生产 116 条)。
 *           照常入库供人工复核,但**不产禁忌**。PAC 不替宿主兜底。
 */
export const ClinicalSignalSchema = z.object({
  /** 字典里的信号码,如 anticoagulant */
  code: z.string().min(1),
  /** 中文标签,如「长期抗凝/抗血小板药物」 */
  label: z.string().min(1),
  /** 命中的原文片段 —— 证据。详情页据此展示医生原话,不是 PAC 的转述 */
  phrase: z.string(),
  /** 原文中的字符区间 [start, end),用于前端高亮 */
  span: z.tuple([z.number().int(), z.number().int()]),
  /** 命中所在的 content 字段名(past_history / name_zh …)—— 溯源到具体哪一段 */
  field: z.string().min(1),
  /** 三态,见上 */
  negated: z.boolean().nullable(),
  severity: SignalSeveritySchema,
  contraindicates: z.array(TreatmentDomainSchema),
  /**
   * 硬失效日(ISO date)。null = 不会自行失效(终身型如放疗,或 review 型)。
   * 已过期的信号**照样入库** —— 2016 年那条「哺乳期」是真发生过的事实,不该被抹掉;
   * 由消费方按 `validUntil > now` 过滤。这是「只讲事实」与「别用过期事实下结论」的分工。
   */
  validUntil: z.string().nullable(),
  /**
   * 复评日(ISO date)。过了这天状态可能已变(停药 / 牙周已控制)→ 消费方把它**降级为
   * check**,而不是让它消失。「消失」会让客服以为患者从没有过这个病史,那是丢事实。
   *
   * ⚠️ 存日期而不是 `needsReview: boolean` —— 布尔值依赖计算时刻,写进库当天就开始变质。
   */
  reviewAfter: z.string().nullable(),
  /** 抽取器版本(clinical-signals@2026-07-27)。字典改版后可按版本定位待重扫的 fact */
  extractedBy: z.string().min(1),
});
export type ClinicalSignal = z.infer<typeof ClinicalSignalSchema>;

/** content.clinical_signals 的数组形态。字段**可选** —— 没命中任何信号的 fact 不写这个 key,
 *  content hash 不变 → 不产生无谓的 fact 版本波。 */
export const ClinicalSignalsSchema = z.array(ClinicalSignalSchema);

/** 治疗域 → 业务侧「X 禁忌」标签。业务方的四分类在这里才出现,不进字典。 */
export const CONTRAINDICATION_LABEL: Record<TreatmentDomain, string> = {
  implant: '种植禁忌',
  surgery: '手术禁忌',
  anesthesia: '麻醉禁忌',
  orthodontic: '正畸禁忌',
  xray: '拍片禁忌',
};

/** 展示顺序 —— 风险从高到低,前端与话术共用,避免两处各排各的 */
export const CONTRAINDICATION_DOMAIN_ORDER: TreatmentDomain[] = [
  'surgery',
  'implant',
  'anesthesia',
  'orthodontic',
  'xray',
];
