/**
 * fact-block —— 厚输入事实块(标准 / 深度档共用的 user-prompt 素材)。
 *
 * 把"聚焦病历 + 其他 reason + 近期治疗 + 目标 + 称呼/医生/日期"拼成自然语言事实块。
 * 各档的**输出指令**(去模板自由写 / 拆大纲 / 多段)在各自 system(format.md)里,user 只给事实。
 * 病种**只给 canonical 名**(去文案模板),风险/好处/检查内容由 LLM 结合病历自供措辞。
 *
 * ⭐ 占位约定(标准/深度=去模板档):事实用**朴素中文标签**直接给(称呼/本次问题/牙位…),
 *   LLM 自然取用,**不用 `{}` 替换占位**(那是稳健模板档的填空机制,去模板档用了反而诱导模板化)。
 *   仅保留 `【】` 输出占位:`【时间段1/2】【具体预约时间】`(客服手填,诊所无排班接口)、
 *   `【回访客服】`(系统渲染期按登录人回填,见 agent-identity.ts)。两类对 LLM 的契约一样:原样输出。
 */
import type { DraftPlanScriptInput, ScriptMedicalRecord } from './input.types';
import { smartDateDisplay, toothFriendly } from './script-facts';
import { resolveDiseaseLabel } from './disease-knowledge';
import { deidentifyDoctor } from './pii';
import { AGENT_IDENTITY_PLACEHOLDER } from './agent-identity';
import { DENTURE_FIRST_AGE, IMPLANT_LAST_AGE, EARLY_ORTHO_MAX_AGE } from '@pac/types';

/**
 * 本批次福利 —— 作为**事实**给 LLM,让它自然融进话术(T4)。
 *
 * ⚠️⚠️ 护栏是这段的重点,不是可选的修饰:
 * 模型拿到"8月种植体检免费"这一句,极容易顺手补成"限本月前20名""老客户专享"
 * "可叠加其他折扣"——**那就是对患者做出了一个不存在的承诺**,患者按它上门,
 * 前台兑现不了。这跟"高龄不主推种植""低龄不承诺能不能种"是同一类硬约束,
 * 落点也放在一起。
 *
 * 没配福利 → 返回空串,**整段不生成**,不留空钩子(免得模型自己去编一个)。
 *
 * ⭐ **导出**:稳健档(tiers/stable/prompt)也用同一份。护栏文案只能有一处 ——
 *   两处各写一份,改了其中一处另一处就悄悄留在旧版本上,而"哪一档漏了哪条禁令"
 *   要等到客服照着念了才发现。
 */
export function benefitBlock(benefit?: { text: string } | null): string {
  const text = benefit?.text?.trim();
  if (!text) return '';
  return `\n\n## 本次福利(硬约束)
- 本批次配了福利,原文如下(**只有这一句是真的**):
  「${text}」
- 可以自然地融进话术,但**只能陈述这句话本身包含的内容**。
- ⛔ **不得追加任何条件、期限、名额、人群限定**(如"限本月""前20名""老客户专享")。
- ⛔ **不得改写金额/折扣/项目,不得夸大,不得暗示还能再优惠**。
- ⛔ 原文没写的一律不说;患者追问细节 → "具体以到院时前台说明为准"。`;
}

export function buildRichFactBlock(input: DraftPlanScriptInput): string {
  const { patient, clinicName, plan, clinicalContext } = input;
  const now = new Date();

  const top = plan.reasons[0];
  const salutation = patient.salutation;
  const doctorSurname = deidentifyDoctor(top?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? null);
  // ⭐ 开场"自从…来过之后"锚【最近一次就诊】(可能晚于诊断日);诊断日更早,归病历/告知用,不放开场
  const lastVisitDate =
    clinicalContext.daysSinceLastVisit != null
      ? new Date(now.getTime() - clinicalContext.daysSinceLastVisit * 86400_000)
      : null;
  /**
   * ⛔ 拿不到末诊就说「上次」,**不许回落到诊断日**。
   *
   * 原来这里有一段 `?? top.triggerDate` 的回落 —— 它把**诊断日**贴上「最近一次就诊」的标签,
   * LLM 就会说"自从您[诊断日]来过之后",而患者在那之后完全可能又来过。
   * 实测(2026-08-05 本地库):2,714 条在跑单里 **788 条(29%)** 患者末诊比诊断晚 30 天以上,
   * 这两个日期差得很远,混用就是说错话。
   *
   * 当前数据每个患者都有就诊记录,所以那段回落一次都没触发过 —— 是颗**哑弹**:
   * 遇到摄入不全的宿主(只有诊断没有接诊记录)就会炸,且不报错。
   * 口径同「⛔ 不明确的不要输出」:宁可模糊说"上次",不拿另一个语义的日期冒充。
   *
   * 诊断日**另有出口** —— 病历块的「接诊日期」(见下方 mrBlock),两个时间各司其职。
   */
  const lastVisitDisplay = smartDateDisplay(lastVisitDate, now) ?? '上次';
  const chiefComplaint = top?.medicalRecord?.chiefComplaint ?? clinicalContext.lastChiefComplaint ?? null;

  const diseaseLabel = resolveDiseaseLabel(top ?? null, plan.primaryScenarioLabel);
  const toothText = top?.toothPositions?.length ? toothFriendly(top.toothPositions) : null;
  const goal = plan.goal?.trim() || null;

  // 自报家门:身份是「诊断医生的助理」不是客服(业务口径 2026-07-29,见 agent-identity.ts);
  //   姓名**不进 prompt** —— 给占位符,LLM 原样输出,读详情时按当前登录人回填
  //   (话术缓存 per-plan、召回池共享,烤进人名会让后开的人读到别人的名字)。
  //   医生姓取自诊断医生;取不到时 deidentifyDoctor 给"您的主治" → "我是您的主治医生的助理X"。
  const selfIntro = `我是${doctorSurname}医生的${AGENT_IDENTITY_PLACEHOLDER}`;
  const guardianHint = patient.guardian
    ? `本次电话打给${patient.guardian.relationshipLabel}(称呼见上方"称呼"),沟通对象是家长,患者是孩子,话术里称孩子为"宝宝"`
    : null;

  const mrBlock = renderMedicalRecord(top?.medicalRecord ?? null);

  const others = plan.reasons.slice(1).map((r) => {
    const t = r.toothPositions?.length ? `(${toothFriendly(r.toothPositions)})` : '';
    return `${resolveDiseaseLabel(r, r.scenarioLabel)}${t}`;
  });

  // 历史治疗:给结构化要素(类目/术式/医生/时间),口语化交给 LLM —— 别照搬这串,自然带一句即可
  const recent = clinicalContext.recentTreatments.map((t) =>
    [t.date, t.categoryLabel, t.subtype, t.doctorName ? `${t.doctorName}医生` : '']
      .filter(Boolean)
      .join(' / '),
  );

  const g = (patient.gender ?? '').trim().toUpperCase();
  const genderText = g === '男' || g === 'M' ? '男' : g === '女' || g === 'F' ? '女' : '';
  const basics = [genderText, patient.age != null ? `${patient.age}岁` : ''].filter(Boolean).join('，');
  // 熟络度信号(给原始事实,不贴"新/老"二分 —— 召回患者基本都有历史,二分会全判"老客"):
  //   完成治疗次数(关系深浅,辅)+ 距上次就诊天数(还熟不熟,主)。tone 交 LLM 按 recency 判断。
  const relationSignal = `已完成 ${clinicalContext.completedTreatmentCount} 次治疗;距上次就诊 ${
    clinicalContext.daysSinceLastVisit ?? '未知'
  } 天`;
  const noXray = patient.age == null || patient.age <= 18;
  // 高龄介绍顺序:活动义齿在前、种植并行 —— 高龄**不是种植禁忌**(业务方口径:
  //   70-80 能否种植看身体条件与患者意愿,由医生评估)。PAC 只调整介绍先后,不替医生下结论。
  //   口径与召回建议治疗同源(DENTURE_FIRST_AGE)。
  const dentureFirst = patient.age != null && patient.age > DENTURE_FIRST_AGE;
  // 低龄种植闸(口径同召回建议治疗 IMPLANT_LAST_AGE):颌骨没长完种植体会相对埋没,
  //   属临床相对禁忌 —— 话术不主推种植,落到"来院评估"。与上面高龄块互斥。
  const implantLast = patient.age != null && patient.age <= IMPLANT_LAST_AGE;
  // 早矫措辞(口径同 treatmentCategoryNameZhFor EARLY_ORTHO_MAX_AGE):替牙期讲"早期矫治"。
  const earlyOrtho = patient.age != null && patient.age <= EARLY_ORTHO_MAX_AGE;

  return `# 本次回访患者信息(只能用以下事实,不要编造或推断额外信息;下面是朴素事实,自然取用即可)

## 开场用
- 称呼:${salutation}
- 自报家门:${selfIntro}(${AGENT_IDENTITY_PLACEHOLDER} 原样保留,别替换成具体姓名,也别自己编)
- 诊所名(**只在需要提到诊所时用这个**,别自己编"XX口腔";自报家门里不用带):${clinicName}
- 最近一次就诊(用于开场"自从…来过";诊断日见病历、可能更早):${lastVisitDisplay}
- 那次主诉:${chiefComplaint ?? '无记录'}
- 诊断医生:${doctorSurname}医生${guardianHint ? `\n- 触达说明:${guardianHint}` : ''}

## 本次应治未治(只讲这一个;病种只给名,风险/好处/检查内容你结合下方病历用自己的话说)
- 本次问题:${diseaseLabel}${toothText ? `\n- 牙位:${toothText}(已是患者口语俗称,直接用)` : ''}

## 病历(诊断上下文 — 接地素材,可引用医生原话体现关怀;⚠️ 只引用、不演绎、不报价、不承诺;与本次无关的别提)
${mrBlock || '- (本次诊断无关联病历,按本次问题 + 牙科常识温和提醒)'}

## 本次目标(内部参考 — 指导复查/邀约方向,不要逐字念给患者)
- ${goal ?? '邀约来院复查,让医生评估本次问题'}
${others.length ? `\n## 其他可一并关心的问题(以本次聚焦为主,自然可顺带;不展开)\n- ${others.join('、')}` : ''}${
    recent.length ? `\n\n## 近期做过的治疗(下面是结构化要素,**用你自己的话自然带一句**体现"诊所记得 ta",别照搬格式;不要重复邀约已做过的)\n${recent.map((r) => `- ${r}`).join('\n')}` : ''
  }

## 患者
- ${basics}
- 熟络度:${relationSignal}(语气怎么拿捏见沟通知识,你按这信号判断)${noXray ? '\n\n## 安全(硬约束)\n- 本患者未满 18 岁或年龄未知:**整篇严禁出现"拍片/拍个片/X光/牙片"等任何拍片表述**' : ''}${dentureFirst ? `\n\n## 高龄沟通(约束)\n- 本患者 ${patient.age} 岁:缺牙修复**先讲活动义齿**(创伤小、周期短),种植可作为并行选项一起提,\n  但**不要主推手术、不要承诺能不能种** —— 统一落到"来院让医生按身体条件评估";措辞更耐心,可提示家属陪同。` : ''}${implantLast ? `\n\n## 低龄沟通(硬约束)\n- 本患者 ${patient.age} 岁,颌骨尚未发育完成:**不要主推种植**(种植体不随颌骨生长,未成年是相对禁忌)。\n  涉及缺牙先讲**间隙管理 / 正畸方向**,修复方式统一落到"来院让医生按发育情况评估",不承诺能不能种。` : ''}${earlyOrtho ? `\n\n## 矫治措辞(约束)\n- 本患者 ${patient.age} 岁处替牙期:涉及矫正一律说「**早期矫治**」(干预颌骨发育与间隙管理),\n  不要说成给恒牙列排齐的"正畸/戴牙套";具体做不做、做哪种,落到"来院让医生评估"。` : ''}${benefitBlock(input.benefit)}`;
}

/**
 * 客户画像 → 话术指引块。从 16 个画像特征里挑对话术有用的,按用途排序:
 *   ⚠注意(禁忌/治疗敏感/特别关注)在前 → 定语气(rfm/lifecycle)→ 切入点(治疗史/权益/折扣/转介/家庭)。
 * 只渲染命中的特征(persona 不产空标签),description 自包含 + 每 key 附一句话术指引。
 * (修:旧版只读已删的 value/recall_risk key → 画像对话术全暗;现消费真实 16 特征。)
 */
const SCRIPT_PERSONA_GUIDE: Record<string, { warn?: boolean; guide: string }> = {
  // description 形如「手术禁忌 / 种植禁忌(长期抗凝药)· 待核查:糖尿病史(控制状态未知)」——
  // 两段语义完全不同,指引必须分开说,否则模型会把「待核查」也当成不能做:
  //   「X 禁忌」= 别推该项目;「待核查」= 只是有病史、控制状态未知,该问一句而不是回避。
  // 且一律不在电话里替医生下结论(能不能做由医生按身体条件评估),这与高龄种植口径同源。
  contraindication: {
    warn: true,
    guide:
      '安全红线:标了「X 禁忌」的项目本次不要推荐、不要承诺;标了「待核查」的只是有相关病史' +
      '(控制状态未知),自然问一句近况即可,别说成不能治。任何情况下都不在电话里判定能不能做,统一落到"来院让医生评估"',
  },
  treatment_sensitivity: { warn: true, guide: '沟通安抚:强调无痛/麻醉/慢节奏,先共情再谈治疗' },
  special_attention: { warn: true, guide: '服务注意:不可等候→提前确认时间;免打扰→克制频次' },
  rfm: { guide: '据分群定语气:重要/高价值→更上心维系、可适度尊享;低活跃→先唤醒别硬推' },
  lifecycle_stage: { guide: '久未到诊(流失/沉睡)→先重新拉近、别急着推治疗' },
  treatment_history: { guide: '可借"上次同类治疗体验"建立信任、跨科室迁移' },
  entitlement_status: { guide: '可切入:商保直付 / 储值权益,降低决策门槛' },
  discount_anchor: { guide: '谈优惠以此历史折扣为底线,优先换推增值权益而非直接降价' },
  referral_champion: { guide: '转介达人→先致谢,体验好可再邀转介' },
  family_structure: { guide: '可切入家庭套餐 / 家人同诊' },
  time_preference: { guide: '约时间时优先客户习惯时段(更易接通/到诊)' },
};
// 精简版(稳健/标准档):只给 ⚠安全 + 定语气 —— 都是"约束/护栏"类,不诱导加推销;
//   全量版(深度档):再叠 切入点(治疗史/权益/折扣/转介/家庭/时间)给"分析"步发挥。
const GUIDE_ORDER_ESSENTIAL = [
  'contraindication',
  'treatment_sensitivity',
  'special_attention',
  'rfm',
  'lifecycle_stage',
];
const GUIDE_ORDER_FULL = [
  ...GUIDE_ORDER_ESSENTIAL,
  'treatment_history',
  'entitlement_status',
  'discount_anchor',
  'referral_champion',
  'family_structure',
  'time_preference',
];

export function buildPersonaGuide(
  highlights: Array<{ key: string; label: string; description: string }>,
  mode: 'full' | 'essential' = 'full',
): string {
  const order = mode === 'essential' ? GUIDE_ORDER_ESSENTIAL : GUIDE_ORDER_FULL;
  const byKey = new Map(highlights.map((h) => [h.key, h]));
  const lines: string[] = [];
  for (const key of order) {
    const h = byKey.get(key);
    const g = SCRIPT_PERSONA_GUIDE[key];
    if (!h || !g) continue;
    lines.push(`- ${g.warn ? '⚠ ' : ''}【${h.label}】${h.description} → ${g.guide}`);
  }
  if (!lines.length) return '';
  const title =
    mode === 'essential'
      ? '## 客户画像(内部参考:⚠注意事项 + 定语气)'
      : '## 客户画像(内部参考:⚠注意事项 + 定语气 + 切入点)';
  return `${title}\n${lines.join('\n')}`;
}

/**
 * 深度档专属扩展块(base++)—— 叠在 buildRichFactBlock 之后,给"分析(plan)"步更全的信息。
 *   ① 价值 / 流失风险(persona)→ 定语气;② 历史联系(诊所最近回访,≤5)→ 避免重复、接住上下文。
 *   标准档不用(标准 = base+)。返回空串时调用方不追加。
 */
export function buildDeepExtensions(input: DraftPlanScriptInput): string {
  const blocks: string[] = [];

  const personaBlock = buildPersonaGuide(input.personaHighlights ?? []);
  if (personaBlock) blocks.push(personaBlock);

  const ch = input.clinicalContext.contactHistory ?? [];
  if (ch.length) {
    const lines = ch
      .map((c) => {
        const head = [c.date, c.type, c.status, c.treatmentItems].filter(Boolean).join(' · ');
        const body = [c.followContent, c.result ? `结果:${c.result}` : ''].filter(Boolean).join(';');
        return `- ${head || '(无日期)'}${body ? ` — ${body}` : ''}`;
      })
      .join('\n');
    blocks.push(
      `## 历史联系(诊所最近的回访/联系记录,最多 5 条 — 别重复人家上次已说过的、可自然接上;⚠️ 只参考不照搬)\n${lines}`,
    );
  }

  return blocks.join('\n\n');
}

/** 聚焦病历 → 接地素材块(SOAP 全字段;空字段略),字段集对齐前端"病历快读" */
export function renderMedicalRecord(mr: ScriptMedicalRecord | null): string {
  if (!mr) return '';
  const lines: string[] = [];
  if (mr.date) lines.push(`- 接诊日期:${mr.date}`);
  // S 主观
  if (mr.presentIllness) lines.push(`- 现病史:${mr.presentIllness}`);
  if (mr.pastHistory) lines.push(`- 既往史:${mr.pastHistory}`);
  if (mr.generalCondition) lines.push(`- 一般情况:${mr.generalCondition}`);
  // O 客观
  if (mr.examFindings.length) {
    lines.push(
      `- 检查所见:${mr.examFindings
        .map((e) => (e.toothPosition ? `${toothFriendly(e.toothPosition)} ${e.message}` : e.message))
        .join(';')}`,
    );
  }
  // A 评估
  if (mr.diagnoses.length) {
    lines.push(
      `- 当次诊断:${mr.diagnoses
        .map((d) => `${d.nameZh ?? d.code ?? ''}${d.toothPosition ? `(${toothFriendly(d.toothPosition)})` : ''}`)
        .filter(Boolean)
        .join('、')}`,
    );
  }
  if (mr.diagnosisText) lines.push(`- 诊断说明:${mr.diagnosisText}`);
  // P 计划
  if (mr.disposals.length) {
    lines.push(
      `- 处置:${mr.disposals
        .map((d) => (d.toothPosition ? `${toothFriendly(d.toothPosition)} ${d.message}` : d.message))
        .join(';')}`,
    );
  }
  if (mr.doctorAdvice) lines.push(`- 医嘱(原话):${mr.doctorAdvice}`);
  if (mr.recommendations.length) {
    lines.push(
      `- 医生建议:${mr.recommendations
        .map((r) => (r.toothPosition ? `${r.text}(${toothFriendly(r.toothPosition)})` : r.text))
        .join(';')}`,
    );
  }
  const treatPlan = renderTreatmentPlan(mr);
  if (treatPlan) lines.push(`- 治疗计划:${treatPlan}`);
  return lines.join('\n');
}

/** 治疗计划文本 —— 结构化 plannedTreatments(主)+ host 自由文本 treatmentPlanText(常空,补) */
export function renderTreatmentPlan(mr: ScriptMedicalRecord): string | null {
  const parts: string[] = [];
  if (mr.plannedTreatments.length) {
    parts.push(
      mr.plannedTreatments
        // subtype 更具体(如"充填")优先;无则用 canonical 类目名(如"充填 / 嵌体")
        .map(
          (t) =>
            `${t.subtype || t.categoryLabel}${t.toothPosition ? `(${toothFriendly(t.toothPosition)})` : ''}`,
        )
        .join('、'),
    );
  }
  if (mr.treatmentPlanText) parts.push(mr.treatmentPlanText);
  return parts.length ? parts.join(';') : null;
}
