/**
 * DraftPlanScript AiCall 的输入契约。
 *
 * harness 原则:input 只装 AiCall 真正用到的字段(渐进式披露),
 * 不要把全量 patient / persona 塞进来污染 context。
 *
 * 装配责任在 orchestrator,见 plan-script.orchestrator.ts。
 */
import type { Tone } from './tone';

// ⭐ 话术上下文(tier-agnostic):稳健/标准/深度三档共用同一份 ScriptContext;
//   也被实时教练复用(realtime-coach-context)。脊柱 = AiCallRunner,策略 = 各档 AiCall。
//   设计见 apps/pac-docs/content/docs/algorithms/script-generation.mdx。
export interface ScriptContext {
  // 患者数据(ScriptContext = 三档全集数据,各档组装 user prompt 时各取所需;非审计快照)。
  //   ⚠️ ID 类不进(关联走 agent_invocations.linked_patient_id 列);真名 name 是数据,
  //   但**不进 user prompt**(走 token 回填),只供内部/深度档更个性化时取用。
  patient: {
    /** 患者真名(如"张震校")— 数据;不直出 user prompt,话术称呼走 salutation/token */
    name: string | null;
    /** 通话称呼(单一源:年龄+性别+监护人 aware)— 未成年=监护人称呼/家长,成人=先生/女士。原 nameMasked */
    salutation: string;
    gender: string | null;
    age: number | null;
    /** 病历号(host 病历主键,如 "FY0A000922")— 供客服核对身份,可空 */
    medicalRecordNumber?: string | null;
    /** 监护人(未成年触达对象)— 让话术知道"打给家长、患者是孩子(称宝宝)";成人/无监护人 → null。
     *  relationship=原始键('mother'/'father'/'grandparent',结构化判断用);relationshipLabel=中文显示("妈妈")。 */
    guardian?: { relationship: string; relationshipLabel: string; name: string | null } | null;
  };

  /** 诊所名 —— 防 LLM 编造"XX口腔"的锚;⚠️ 自报家门里**不用**它(身份是"{诊断医生}医生的助理X") */
  clinicName: string;

  /**
   * 本批次配的福利(来自 plan_assignments.attributes.benefit)。null = 没配 → **整段不生成**。
   *
   * ⭐ 走 prompt 输入而不是确定性占位符,是有意的(T4):
   *   占位符那套(`AGENT_IDENTITY_PLACEHOLDER`)是给 **PII 与缓存**用的
   *   —— 人名不进 LLM、换客服不用重生成。福利是**内容**不是身份 token,
   *   硬插一句会打断口语流;且三档输出形态差异大(稳健=模板填空 / 标准=自由段落 /
   *   深度=多段分析),占位符要在每档各实现一次。作为事实输入则三档通用。
   *
   * ⚠️⚠️ **必须带护栏**:LLM 拿到一句"8月种植体检免费"极容易顺手补成
   * "限本月前 20 名""老客户专享""可叠加折扣" —— 那就是**对患者做出不存在的承诺**。
   * 护栏落点与"高龄义齿 / 低龄种植"那两条年龄约束同款,见 fact-block 与 stable/prompt。
   *
   * ⚠️ 话术缓存是 per-plan(planId @unique),福利一变必须作废缓存,
   * 否则客服会照着上一个批次的福利念。见 plan-assignment.service.invalidateScripts。
   */
  benefit?: { text: string } | null;

  /** ⚠️ 这里**没有** agent(回访客服)字段,是刻意的 —— 姓名不进 LLM 输入。
   *  话术缓存是 per-plan(UNIQUE plan_id)、召回池又共享,烤进人名会让后开的客服读到别人的名字。
   *  改成「生成期占位 `【回访客服】` → 渲染期按登录人回填」,见 shared/agent-identity.ts。
   *  副作用(正向):input_hash 不含人名 → 同 plan 不同客服命中同一 AI 缓存。 */

  /** Plan 信息 */
  plan: {
    /** 主场景 label(从 scenario 枚举翻译,如"治疗后复诊召回"/"漏治-缺失牙"等) */
    primaryScenarioLabel: string;
    /** ⭐ 主场景 raw key(skill composer.applies.scenario 用,如 'treatment_initiation_recall') */
    primaryScenarioKey: string | null;
    priorityScore: number;
    /** ⭐ 本次召回的明确目的(plan.goal 原文,如"邀约做牙周基础治疗(SRP/翻瓣),控制炎症发展")
     *  让 LLM followup 段对齐该目标,不再自己脑补"我们想约您来评估" */
    goal: string | null;
    /** 触发原因(最多 3 条)。⭐ 契约:**已按 priorityScore 降序**,`reasons[0] = 本次话术聚焦项**
     *  (单一聚焦,三档共用此选择;orchestrator 单一排序,消费方直接取 [0],不再各自 sort)。
     *  其余 reasons 供标准/深度档"可一并邀约的其他牙问题"用。 */
    reasons: Array<{
      scenarioLabel: string;
      /** ⭐ 子场景 base key(去 @tooth 后缀,如 'caries_no_filling';skill composer 推 dxCode 用) */
      subKey: string | null;
      /** ⭐ ICD-10 K-code(K00-K09,skill composer.applies.diagnosisCodePrefix 用) */
      dxCode: string | null;
      /** ⭐ 最高优先触发源(诊断>建议>影像,来自 plan_reason.signals.triggers[0])。
       *  type='recommendation' 且无诊断 ⟹ 该 gap 仅医生建议 → 病种文案用建议名(不套"缺失牙"诊断词)。 */
      leadTrigger: { type: string; code: string } | null;
      /** ⭐ 涉及牙位(FDI 号,结构化;来源 plan_reason.signals.toothPosition 拆分)。
       *  空数组 = 全口/无具体牙位。俗称("上门牙")由消费方按需渲染,不在快照里拍平。
       *  缺失牙位占位等场景从此取牙位,不再丢进散文。 */
      toothPositions: string[];
      reason: string;
      priorityScore: number;
      /** 触发该诊断/建议的医生(LLM 在 followup 段必须引用此人,不要用 primaryDoctorName)
       *  来源:reason.evidence.factIds[0] → patient_facts.content.doctor_name
       *  null = 该 fact 无医生记录 → LLM fallback "您的主治医生" 泛指 */
      triggerDoctor: string | null;
      /** 触发诊断/建议的日期(YYYY-MM-DD),给 LLM 在话术里用,如"上次姜医生 X 月 X 日给您检查时...") */
      triggerDate: string | null;
      /** ⭐ 病历 —— 该诊断**那次接诊**的完整病历(SOAP 全字段)。每条 reason 各带自己那次接诊的病历;
       *  reasons[0].medicalRecord = 聚焦诊断的病历。主诉(chiefComplaint)等从这里取,不再单列。null = 无关联病历。 */
      medicalRecord: ScriptMedicalRecord | null;
    }>;
  };

  /** Persona 关键特征(只挑相关的,不全量) */
  personaHighlights: Array<{
    key: string;            // 原始 feature_key,如"value"(结构化判断用)
    label: string;          // 中文 label,如"客户价值"(显示用)
    description: string;    // persona 层 canonical 描述(design §5.2 故意不拆字段),如"高价值(累计消费 ¥35,000)"
  }>;

  /** 临床事实摘要(只挑最近 / 跟召回相关的) */
  clinicalContext: {
    daysSinceLastVisit: number | null;
    /** 上次到店(结构化:date=年月文本"2024.03" / summary=做了什么);整体可空。
     *  ⚠️ 原 lastVisitSummary 是拍平散文,已结构化;消费方按需拼"{date} — {summary}"。 */
    lastVisit: { date: string | null; summary: string | null } | null;
    /** 上次就诊主诉(emr_record.illness_desc 原文,自由文本直喂 LLM 当上下文;可空) */
    lastChiefComplaint?: string | null;
    /** 待做治疗(reason 派生,即"未启动治疗")。结构化:subKey/label/FDI 牙位;俗称由消费方渲染,不拍平。 */
    pendingTreatments: Array<{
      subKey: string | null;
      label: string;              // 子场景中文 label,如"龋齿未做充填"
      toothPositions: string[];   // FDI 号(结构化);空 = 全口/无具体牙位
    }>;
    /** 主治医生名(从最近 treatment/diagnosis fact 抽);LLM 必须用此名,不可编造 */
    primaryDoctorName: string | null;
    /** ⭐ 近期做过的治疗(结构化:每个 category 一条最新)。
     *  来源 treatment_record(非治疗链);消费方渲染成"做过牙周 · 上次龈上洁治 · 吴医生 · 2024.04"。
     *  LLM 用于:① 不重复邀约已做过的 ② 引用历史显"诊所记得 ta"。
     *  ⚠️ 治疗链(chain-composer)已废弃 → 这里只读"做过什么治疗",不含链/阶段概念。 */
    recentTreatments: Array<{
      category: string;           // canonical category,如"periodontic"
      categoryLabel: string;      // 中文,如"牙周"
      subtype: string | null;     // 术式子类,如"龈上洁治"
      doctorName: string | null;
      date: string | null;        // 年月文本,如"2024.04"
    }>;
    /** ⭐ 已做治疗总次数(信任锚);LLM 用于:老客可以更家常,新客需自报家门更详细 */
    completedTreatmentCount: number;
    /** ⭐ 历史联系(诊所最近的回访/联系记录,患者级,**最多最近 5 条**)。页面"历史联系"同源(patient_return_visits)。
     *  深度档用:避免重复人家已说过的、自然接上上次的上下文。其余档不渲染。 */
    contactHistory: Array<{
      date: string | null;          // task_date YYYY-MM-DD
      type: string | null;          // 回访类型(术后/常规/咨询…)
      status: string | null;        // 已回访 / 未回访
      treatmentItems: string | null;
      followContent: string | null; // 跟进内容
      result: string | null;        // 结果
    }>;
  };
}

/**
 * ScriptMedicalRecord —— 一次接诊的**完整病历**(SOAP 全字段,结构化)。
 * 挂在每个 reason 上(reason.medicalRecord):该诊断那次接诊的病历。字段集对齐前端"病历快读"(emr-soap-view)。
 * ⚠️ 进 input_snapshot(全集),**不等于全进 user prompt** —— 自由文本编造风险高,各档按需引用:
 *   稳健引用医嘱/主诉;标准/深度可取检查所见/处置等更多。
 */
export interface ScriptMedicalRecord {
  date: string | null;                 // 接诊日期 YYYY-MM-DD
  doctorName: string | null;
  // S 主观(自由文本)
  chiefComplaint: string | null;       // illness_desc 主诉
  presentIllness: string | null;       // pre_illness 现病史
  pastHistory: string | null;          // past_history 既往史
  generalCondition: string | null;     // general_condition 一般情况
  // O 客观(按牙位拆)
  examFindings: Array<{ toothPosition: string | null; message: string }>; // exam_findings
  // A 评估(同次接诊的诊断,即被召回的诊断所在)
  diagnoses: Array<{
    code: string | null;
    nameZh: string | null;
    toothPosition: string | null;
    fromImageAi: boolean;              // 影像 AI 抽出(非医生手写)
  }>;
  // P 计划
  disposals: Array<{ toothPosition: string | null; message: string }>;    // disposal 处置
  doctorAdvice: string | null;         // doctor_advice 医嘱
  treatmentPlanText: string | null;    // treatment_plan(host 自由文本字段,常空)
  diagnosisText: string | null;        // diagnosis_text(host 自由文本字段,常空)
  /** 医生建议(同次接诊的 recommendation_record,如"建议拔除18/38/48")—— 常是召回理由本身 */
  recommendations: Array<{ text: string; toothPosition: string | null }>;
  /**
   * 计划治疗(同次接诊的 treatment_record kind=planned,结构化)—— **真正的"治疗计划"**。
   * host 的 treatment_plan 自由文本字段常空,治疗计划落在独立 treatment_record(planned),
   * 故单列一项喂话术(否则话术里"治疗计划"永远缺位,如龋齿已诊断、计划充填未做)。
   */
  plannedTreatments: Array<{ categoryLabel: string; subtype: string | null; toothPosition: string | null }>;
}

/**
 * AiCall 输出契约(B 方案 · 2026-05-24 重写)。
 *
 * LLM 直接输出 4 段 Markdown 字符串 + tone tag。
 * orchestrator 把每段直接映射到前端 ScriptSection,**不再做模板拼接**;
 * 段内子结构(`**目的**:` / `**注意**` / `**异议预判**` / `**回写要点**` / `### A.` ...)
 * 由 LLM 按 schema.ts 的 .describe() 契约自己写。
 *
 * 为什么从"结构化字段+后端拼模板" 改成"LLM 直出 Markdown":
 *   - 段内子结构需求(目的/正文/注意)模板化后失去个性化,套话感重
 *   - 4 段独立 markdown 字段对 zod 仍是强校验(长度 + .describe() 格式契约)
 *   - 流式仍然可行(generateObject partial 阶段每段 string 逐字符流出)
 *   - Flash 模型友好 — schema 字段少,LLM 单字段长 markdown 比多字段嵌套对象更稳
 */
/** @deprecated 用 ScriptContext —— 稳健档输入 = 通用 ScriptContext(保留别名,兼容现有引用) */
export type DraftPlanScriptInput = ScriptContext;

export interface DraftPlanScriptOutput {
  /** 整体语气标签(给客服参考)— 单一源见 shared/tone.ts */
  tone: Tone;

  /** 第一部分·开场白 markdown(以医生名义 + 智能称呼 + 智能日期 + 自报家门) */
  opening: string;

  /** 第二部分·告知应治未治 markdown(成人 4 句/儿童 5 句:现状/风险/关怀/专业建议,只讲单个应治未治项) */
  informMissed: string;

  /** 第三部分·复查建议 markdown(成人 4 句/儿童 5 句:重要性/维护/复查时长/引导预约【时间段】) */
  reviewAdvice: string;

  /** 第四部分·结束回访语 markdown(预约成功 / 不成功 两种) */
  closing: string;

  /** ⭐ 段落标题(标准/深度档:LLM 为 4 段各自起的小标题;稳健档不出 → UI 回退固定标题)。
   *  设计:稳健档标题固定(开场白/告知应治未治/复查建议/结束回访语);标准档"标题不定",由 LLM 编排。 */
  sectionTitles?: {
    opening?: string;
    informMissed?: string;
    reviewAdvice?: string;
    closing?: string;
  };
}
