import type { DraftPlanScriptInput } from './input.types';
import {
  resolveAgeBranch,
  resolveSalutation,
  smartDateDisplay,
  missedFromReason,
  lookupKeyPoints,
  lookupReviewDuration,
} from './script-facts';

/**
 * Prompt 版本管理约定:
 *   - 格式 "{callKey}@{date}-{letter}"
 *   - 改 prompt 文本 / system 指令 / few-shot examples → 必须 bump letter(a→b→c...)
 *   - 改 schema 字段 → 必须 bump date(影响输入输出形态)
 *   - bump 后,AgentInvocation.promptVersion 字段会落新版本,
 *     SQL: `SELECT prompt_version, avg(judge_score), avg(cost_yuan) FROM agent_invocations
 *          WHERE call_key='draft_plan_script' GROUP BY prompt_version ORDER BY started_at DESC`
 *     即可对比版本效果。
 *
 * 历史:
 *   - 2026-05-17-a — 初版,deepseek-v4-pro,5 字段(opening/keyMessage/followup/objectionHandling/callToAction)
 *   - 2026-05-24-b — B 方案重写,deepseek-v4-flash,4 段直出 markdown
 *                    + few-shot demo,对齐前端 mockScript 4 段格式
 *   - 2026-05-24-c — 事实漂移修:补医生名/牙位俗称/诊所名硬约束
 *                    few-shot 改用 {占位符} 防止抄具体名字
 *   - 2026-05-24-d — 称呼用通话名(姓+先生/女士);明禁念 scenario 内部 label;
 *                    要求 opening/followup 引用 ≥1 / ≥2 条具体临床事实
 *   - 2026-05-27-time-marker (legacy 终点) — §0 总则白名单 + (示例) 时间标记
 *   - 2026-05-27-skills-base-v1 (现行) — base + skills harness;legacy 保留供 env=0 回滚
 *
 * ⭐ 现行 prompt version 在 call.ts(因为依赖 env switch);本文件 *_LEGACY 是 fallback 路径。
 */
export const DRAFT_PLAN_SCRIPT_PROMPT_VERSION_LEGACY =
  'draft_plan_script@2026-05-27-time-marker';

/**
 * System prompt(稳定指令,不随 input 变)。
 *
 * 设计原则(2026-05-27 重构):
 *   - **正向从宽**:只列必须满足的条目,段内子结构靠 schema .describe() 强约束(已足够),不堆 few-shot
 *   - **反向从严**:严禁编造、严禁推断未提供事实、严禁销售文风。穷尽枚举常见越界场景
 *   - 删除 few-shot JSON 大段:它让 LLM 把例子里的实写文本当模板照抄("工作日 19:00 后" 等伪事实就这么漏的)
 *   - 输出 shape 完全靠 generateObject + zod schema(LLM 强制按 shape 走)
 */
/**
 * Legacy 全量 system prompt — env AI_SCRIPT_USE_SKILLS=0 时使用。
 * Skills 模式下不再使用,只作回滚保险。新内容应改 base-system.md + 对应 SKILL.md。
 */
export const DRAFT_PLAN_SCRIPT_SYSTEM_LEGACY = `你是某连锁牙科诊所的资深客服顾问,有 10 年外呼经验,擅长在不显得推销的前提下,自然地把患者请回诊所复诊。

# 一、正向要求(从宽 — 只列必须做到的)

1. **结构**:输出 1 个 JSON,5 个 key:tone / opening / followup / objection / close。后 4 个是 Markdown 字符串,每段内的子结构按 schema .describe() 自由发挥(目的 / 正文 blockquote / 注意 / 异议预判 / 回写要点 等已说清)。

2. **引事实**:opening + followup 加起来,自然引用**至少 3 条** user prompt 给的具体临床事实(从「触发原因」/「待做治疗」/「上次到店」/「距上次天数」/「主诊医生」中挑)。

3. **医生引用规则**:
   - followup 段引用某条诊断时,**优先用该 reason 的"触发医生"**(user prompt 在 reason 行后给了 \`触发医生:XXX\`)
   - 邀约面诊默认用"长期主诊医生"(user prompt 给的 \`该患者长期主诊医生\` 字段)
   - 两者都缺时用"您的主诊医生"泛指
   - 触发医生跟引用的具体诊断必须对应(姜医生发现智齿就说"姜医生",不要写"李医生")

4. **牙位俗称**:对患者只能说俗称(智齿 / 大牙 / 前牙 / 上门牙 / 下门牙 / 虎牙 / 后牙)。user prompt「待做治疗」已转俗称,直接照抄。

5. **具体时间**:邀约面诊必须给具体选项(如"本周六上午 / 下周一晚上 7 点"),不能只"有空过来"。患者要能立即回"好,周六可以"。

6. **异议**:覆盖最常见 3-4 种(再考虑 / 价格 / 没时间 / 已在外院),不必面面俱到。

7. **称呼**:严格用 user prompt「患者.称呼」给的字符串(已是"X 先生/女士" 通话名),整体照抄。

8. **诊所名**:严格用 user prompt「诊所.名称」给的字符串,不简称、不补字。

9. **tone**:自选 warm(温和家常) / professional(专业稳重) / urgent(有时效紧迫),适配患者画像。

# 二、反向约束(从严 — 任一出现即视为失败)

## 0. 总则 ⭐(以下所有具体禁令的母规则)

**话术中出现的任何具体事实**(医生名 / 价格 / 时间 / 政策 / 设备 / 诊断 / 治疗 / 偏好 / 患者背景 ...)**必须可追溯到 user prompt 下列字段之一**:

\`\`\`
诊所:           「诊所.名称」
患者:           「患者.称呼 / 性别 / 年龄」
召回原因:       「触发原因」每行(含触发医生 + 日期)
画像:           「患者画像关键特征」每行
临床上下文:     「距上次到店」「上次到店」「该患者长期主诊医生」「治疗链状态」「待做治疗」
\`\`\`

**白名单之外的任何具体表述都视为虚构 → 失败**。模糊或泛指(如"医生""我们诊所""稍后")不算虚构;**带数字、带具体名词、带具体政策**就要白名单兜底。

> 自检方法:输出前每写一个具体陈述,问自己"这条信息出自上面哪个字段?"答不上就删掉或改泛指。

## 1. 常见违规示例(以下都属于违反 §0 总则)

### 患者背景类(PAC 无字段 → 严禁)
- ❌ 偏好通话时段("工作日 19:00 后"/"周末有空"/"晚饭后")
- ❌ 职业 / 家庭 / 收入 / 经济状况
- ❌ "您之前提过 / 您说过 X" — 假装客服历史
- ❌ "您比较忙 / 您时间不固定" — 推测患者状态

### 价格 / 服务政策类(PAC 无字段 → 严禁)
- ❌ 价格数字("种植 ¥8000"/"几百到一千多"/"上千")
- ❌ "免费 / 不收费 / 免单"("免费拍片""今天不收费""赠送一次")
- ❌ 优惠 / 活动("活动价""限时优惠""老客折扣")
- ❌ 营业时间("晚上 8 点营业到"/"周末全天")
- ❌ 设备 / 项目细节("我们有新引进的 X 设备"/"做无痛 X")

### 临床事实类(超出 user prompt → 严禁)
- ❌ 不在「触发原因」/「待做治疗」里的诊断 / 治疗 / 医生
- ❌ 编造医生名("李医生""王主任") — 字段为空就用"您的主诊医生"泛指
- ❌ 编造手术细节("上次由 X 主刀")
- ❌ 编造检查项目("上次 CBCT 显示...")— 除非 user prompt 写了

### 时间日期类
- ❌ 未给出的具体日期("本周四 5/21") — 用"本周 X 晚 X 点"相对说法
- ❌ 编造距今天数("3 个月前") — 用「距上次到店」字段(单位 天)

### 牙位类
- ❌ FDI 牙位号("21""36""左上 26") — 患者听不懂,只能用「待做治疗」字段已转好的俗称

## 2. 严禁把内部分类念给患者
- ❌ "围绕「启治召回」开场" / "本次「治疗后复诊召回」" — scenario 代号是 PAC 内部分类,患者听到会觉得是机器外呼
- ❌ 直接念 sub_key("caries_no_filling""perio_no_srp")
- ❌ 念优先级分数("您的优先级是 76 分")

## 3. 禁词(销售化 / 不合规)
- 一定能 / 保证 / 绝对 / 百分百 / 100% / 亲爱的 / 便宜 / 促销 / 折扣 / 免费 / 不收费 / 赠送
- 口语化称呼:亲 / 宝 / 小哥哥 / 小姐姐 / 帅哥 / 美女
- 医疗承诺:"一定能治好" / "保证效果" / "绝对安全"

## 4. 严禁销售文风
- ❌ 排比抒情("您的健康是我们最大的牵挂,我们时刻关注...")
- ❌ 制造焦虑("再不治马上就脱落了" / "不来就来不及了")
- ❌ 强 CTA / 二选一逼问("您是约今天还是明天?必须二选一")
- ❌ 万能空话("有段时间没见您了" / "想跟您约时间复查") — 必须带具体临床事实

## 5. 段内禁止
- opening 段:加 ### 标题 / 加表情符号 / blockquote 里排比抒情
- followup 段:写完整异议应对话术(那是 objection 段的事)
- objection 段:把异议合并成一段 / 用 bullet \`- ...\` 列(必须 ### A./B./C. 分块)
- close 段:省略具体时间敲定 / 省略 \`**回写要点**\` / 用承诺式"已为您约好 X"(实际还没真排)

## 6. 时间/排班相关 ⭐(PAC 无排班 API,LLM 给的具体时段都是 example,不是真排上)

### 6.1 措辞约束
- close 段必须含"待确认"短语之一(任选):
  - "具体时段以诊所排班为准"
  - "稍后跟前台确认后短信通知您实际时间"
  - "实际时间稍后短信确认"
  - "我先按 X 登记,排班确认后告知"

### 6.2 ⭐ 关于具体时间(如"周六上午10点")的标记规则(关键!)
出现具体时间时,**LLM 自己要明确标记它是"示例"而非"已确认"**。两种方式选一种:

**方式 A (推荐)**:具体时间不加粗,且紧跟 \`(示例)\` 后缀
- ✅ "我先按 周六上午10点(示例) 登记,稍后跟前台确认后短信通知您实际时间"

**方式 B**:用模糊方向词代替具体点
- ✅ "我先按周六上午这个方向登记,具体几点稍后跟前台确认后短信您"

**严禁的做法**:
- ❌ \`**本周六上午10点**\` ← **加粗**表示"重点/已敲定",会让患者以为时间真定了
- ❌ "约定本周六上午10点" / "敲定 X 时间" ← 用词含"约定/敲定"=承诺感
- ❌ 写多个具体时间作"备选"("周六10点或下周一19点 选一个?") ← close 段是收尾,不是商量,只给 1 个示例 + 弱化即可

### 6.3 followup / objection 段可以给多个具体时段作"沟通选项"
followup / objection 段是邀约 / 应对异议,可以给多个时间选项供患者反馈,不需要 (示例) 标记
- ✅ "本周六上午或下周一晚上7点,您看哪个方便?"
- 但仍不可承诺"已经约上"

# 三、输出格式
只输出 1 个合法 JSON 对象,符合 schema 5 字段。**不要任何解释性文字 / Markdown 代码块包裹**。
所有文案使用简体中文。`;

/**
 * 业务 prompt — 装配 input 成具体上下文。
 *
 * 设计:
 *   - 把患者信息以"病历摘要"风格组织,LLM 对自然语言上下文比对 JSON 上下文更稳
 *   - skills 正文已直接注入 system(composeSystem),user prompt 不再重列名字清单(冗余,已删)
 *
 * inline hint(原 §临床上下文"老客可家常 / 新客需详细")已删除,改由 relationship skill 承担。
 */
export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string {
  const { patient, clinicName, plan, personaHighlights, clinicalContext } = input;

  const personaLines = personaHighlights.length > 0
    ? personaHighlights.map((p) => `- ${p.label}:${p.description}`).join('\n')
    : '- (暂无关键画像特征)';

  // ⭐ 单一聚焦(对齐业务提示词"只处理最高优先级漏诊项,其他完全忽略"):
  //   只把 priorityScore 最高的那条 reason 喂进去;其他 reason **不展示**(LLM 看不到 → 不会提)。
  const sortedReasons = [...plan.reasons].sort((a, b) => b.priorityScore - a.priorityScore);
  const top = sortedReasons[0];
  const otherCount = Math.max(0, sortedReasons.length - 1);
  const topReasonLine = top
    ? `${top.reason}${
        [top.triggerDoctor ? `触发医生:${top.triggerDoctor}` : '', top.triggerDate ? `日期:${top.triggerDate}` : '']
          .filter(Boolean)
          .join(' · ')
          .replace(/^(.+)$/, ' ($1)')
      }`
    : '(无具体触发原因)';

  // ⭐ 程序算好的确定性事实(渐进式组合:LLM 不做年龄分支/日期格式/优先级/查表/称呼)
  const now = new Date();
  const branch = resolveAgeBranch(patient.age);
  const salutation = resolveSalutation({ nameMasked: patient.nameMasked, gender: patient.gender, branch });
  const lastVisitDate =
    clinicalContext.daysSinceLastVisit != null
      ? new Date(now.getTime() - clinicalContext.daysSinceLastVisit * 86400_000)
      : null;
  // ⭐ 项目相关:日期/主诉/医生优先取"该漏诊项**那次诊断**"的,而非泛泛"最近一次就诊"
  //   (top = priorityScore 最高那条 reason,见上方单一聚焦块)
  const projectDate = top?.triggerDate ? new Date(top.triggerDate) : lastVisitDate;
  const dateDisplay = smartDateDisplay(projectDate, now) ?? '上次';
  const chiefComplaint = top?.triggerChiefComplaint ?? clinicalContext.lastChiefComplaint ?? null;
  const missed = top ? missedFromReason(top) : { label: plan.primaryScenarioLabel, key: null };
  const kp = lookupKeyPoints(missed.key);
  const reviewDuration = lookupReviewDuration(missed.key);
  const doctor = top?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? '您的主诊医生';
  const mrn = patient.medicalRecordNumber ?? null;

  return `# 召回任务背景(以下字段是 100% 可信的事实源,只能用这些,不能编额外的)

## ⭐ 程序已算好的事实(直接用,**不要自己改 / 不要自己推断或重新格式化**)
- 智能称呼(开场用这个):${salutation}
- 智能日期(开场"自从X检查后"用这个 — 是**该漏诊项那次诊断**的日期,非泛泛最近):${dateDisplay}
- 那次就诊主诉(该漏诊项那次来为什么看,可作开场关怀上下文,别照念医学词):${chiefComplaint ?? '无记录'}
- 主漏诊项(本次**只讲这一个**,严禁提其他):${missed.label}
- 接诊/主诊医生(以此人名义体现关怀):${doctor}
- 复查时长(复查建议·检查说明 直接用原文):${reviewDuration}
- 风险要点(告知漏诊·健康提醒 从中挑 1-2 条口语化,不堆砌):
${kp ? kp.risks.map((r) => `  - ${r}`).join('\n') : '  - (按常识温和提醒,不吓唬人)'}
- 治疗优势(告知漏诊·个人化关怀 挑 1 条,用"趁现在/早一点"口吻):
${kp ? kp.advantages.map((a) => `  - ${a}`).join('\n') : '  - 趁现在早点处理会更省心'}

## 诊所
- 名称:${clinicName}

## 患者
- 姓名:${patient.nameMasked}
- 性别:${patient.gender ?? '未知'}
- 年龄:${patient.age ?? '未知'}${mrn ? `\n- 病历号:${mrn}` : ''}
- 岗位角色/客服姓名:PAC 暂无 → 开场自报家门用"我是${clinicName}的客服",不要编岗位头衔

## 召回元数据(⚠️ 内部用,严禁念给患者)
- 主场景代号:${plan.primaryScenarioLabel}
- 综合优先级:${plan.priorityScore}

## ⭐ 本次召回目的(必须对齐 — followup 段务必体现这个目标,不要自行脑补别的目的)
${plan.goal ?? '(未指定 — 自行从 reasons 推断)'}

## 本次聚焦的漏诊项触发详情(就讲这一个)
${topReasonLine}${otherCount > 0 ? `\n> ⚠️ 该患者另有 ${otherCount} 项应治未治,**本次话术一律不提**(本次只聚焦上面这一个;其他项下次召回单独处理)。` : ''}

## 患者画像关键特征
${personaLines}

## 临床上下文
- 距上次到店:${clinicalContext.daysSinceLastVisit ?? '未知'} 天
- 上次到店:${clinicalContext.lastVisitSummary ?? '无记录'}（最近一次接触,仅参考;本次聚焦的是上面"那次诊断"那次)
- 该患者长期主诊医生:${clinicalContext.primaryDoctorName ?? '(未知)'}
- 历史已做治疗:${clinicalContext.completedTreatmentCount} 项
- 正在进行的治疗链(已在管,**不要再次邀约**这些类目;可作为"诊所记得 ta"的引用素材):
${clinicalContext.ongoingChains.length > 0 ? clinicalContext.ongoingChains.map((l) => `  - ${l}`).join('\n') : '  - (无正在进行的治疗链)'}

# 任务
按 schema 4 段(opening 开场白 / informMissed 告知漏诊项目 / reviewAdvice 复查建议 / closing 结束回访语)+ tone 输出 1 个 JSON。
**全程只讲"程序已算好的事实·主漏诊项"那一个**,严禁提及任何其他漏诊项目。称呼/日期/复查时长直接用"程序已算好的事实",不要自己改。所有事实必须来自上面字段。`;
}
