/**
 * safety-rules —— 话术安全的**单一真理源**(tier-agnostic,三档共用机器闸 + prompt)。
 *
 * 收口决策(2026-06):此前禁词散在两处且不一致(base-system.md 一份 + call.ts FORBIDDEN_PHRASES 一份)。
 *   现统一到本文件:
 *     - 机器闸:各档 AiCall.safetyRules = SCRIPT_SAFETY_RULES(本文件)
 *     - prompt:system 里的禁词块由 forbiddenWordsBlock() 生成,base-common.md 不再各写一份
 *   改一处,机器闸 + 提示词同步。
 */
import type { SafetyRule } from '../../../core/safety-gate.service';
import type { DraftPlanScriptOutput } from './input.types';

// ⚠️ 单字符禁词务必避免(会误伤合法词):'亲'→亲切/母亲;'宝'→宝贝/宝宝。只保留"销售化"明确组合形式。
export const FORBIDDEN_PHRASES = [
  '一定能', '保证', '绝对', '百分百', '100%',
  '亲爱的',          // 淘宝式称呼
  '便宜', '促销', '折扣', '免费', '不收费', '赠送',
] as const;

// 承诺式表述(PAC 无排班 API,时间不能写死/承诺)
export const COMMIT_PHRASES = [
  '已为您约好', '已成功预约', '已为您预约', '已经为您约', '已替您预约',
  '约定本', '敲定本', '安排好了', '已经预约',
] as const;

// 加粗具体时间(误导"已定");新结构应保留【时间段】占位
const BOLD_TIME_REGEX = /\*\*[^*\n]*(?:周[一二三四五六日天]|\d+\s*(?:点|:|：))[^*\n]*\*\*/;

// 内部代码/术语泄漏到患者话术(患者听不懂):
//   - 诊断代码:K 开头 + 2 位数字(可带小数),如 K08 / K05.1(本域诊断码 K00–K14)
//   - 内部枚举:全大写下划线,如 IMPLANT_RECOMMENDED / TREATMENT_INITIATION
//   保留占位标签【…】不算(那是给客服手填的,不在此列)。
const CLINICAL_CODE_REGEX = /K\d{2}(?:\.\d+)?|[A-Z]{2,}_[A-Z0-9_]+/;

/** 拼整篇 4 段文本(机器闸全文扫) */
function fullText(o: DraftPlanScriptOutput): string {
  return [o.opening, o.informMissed, o.reviewAdvice, o.closing].join('\n');
}

/**
 * 机器安全扫描(纯函数,任意文本)—— 深度档策略侧用(在 repair 决策点调,而非 runner per-call gate)。
 * 返回问题列表;空 = 通过。
 */
export function machineSafetyScan(text: string): string[] {
  const problems: string[] = [];
  const f = FORBIDDEN_PHRASES.filter((p) => text.includes(p));
  if (f.length) problems.push(`禁词: ${f.join(',')}`);
  const c = COMMIT_PHRASES.filter((p) => text.includes(p));
  if (c.length) problems.push(`承诺式表述: ${c.join(',')}`);
  if (BOLD_TIME_REGEX.test(text)) problems.push('加粗了具体时间(应保留【时间段】占位)');
  const code = text.match(CLINICAL_CODE_REGEX);
  if (code) problems.push(`出现内部代码/术语"${code[0]}"(患者听不懂)— 改用大白话,如"缺了一颗小磨牙"`);
  return problems;
}

/**
 * prompt 用的禁词块(system 注入;与机器闸同源,避免漂移)。
 *
 * @param opts.timePlaceholders 是否保留【时间段】占位。
 *   ⭐ 电话档 `true`(客服边打边填);⛔ **企微档必须 `false`** ——
 *   企微那条消息是整段复制直接发出去的,占位符会原样发给患者。
 *   ⚠️ 这个参数存在的唯一原因是:本块与企微的 format.md 会拼进**同一份 system**,
 *   不参数化就等于给模型两条互相矛盾的指令(一边说"照旧保留"、一边说"一个都不许出现"),
 *   而它照哪条做全看运气。
 */
export function forbiddenWordsBlock(opts: { timePlaceholders?: boolean } = {}): string {
  const keepPlaceholders = opts.timePlaceholders ?? true;
  return [
    '# 禁词(整篇严禁出现)',
    FORBIDDEN_PHRASES.join(' / '),
    '以及:"一定能治好" / "保证效果" / "绝对安全" 等承诺;"亲 / 宝 / 帅哥 / 美女" 等淘宝式称呼。',
    '',
    '# 说人话(患者听得懂)',
    '严禁把内部代码 / 专业术语原样念给患者:**不出现诊断代码(如 K08、K05.1)、英文或全大写下划线枚举(如 IMPLANT_RECOMMENDED)**。',
    '一律翻成大白话:"K08" → "缺了一颗小磨牙";牙位/诊断说成患者能懂的位置和说法。' +
      (keepPlaceholders
        ? '占位标签【时间段】等照旧保留(那是给客服填的)。'
        : '⛔ 本篇**不许出现任何 `【】` 占位标签**(除自报家门里的【回访客服】),尤其是时间占位。'),
  ].join('\n');
}

/** 三档共用的机器安全规则(后置硬约束;LLM 偶尔越过 schema 塞禁词,这里兜底) */
export const SCRIPT_SAFETY_RULES: ReadonlyArray<SafetyRule<DraftPlanScriptOutput>> = [
  {
    name: 'no_forbidden_phrases',
    severity: 'block',
    check(output) {
      const text = fullText(output);
      const hit = FORBIDDEN_PHRASES.filter((p) => text.includes(p));
      return { pass: hit.length === 0, message: hit.length ? `命中禁词: ${hit.join(',')}` : undefined };
    },
  },
  {
    name: 'no_commit_phrasing',
    severity: 'block',
    check(output) {
      const text = [output.reviewAdvice, output.closing].join('\n');
      const hit = COMMIT_PHRASES.filter((p) => text.includes(p));
      return {
        pass: hit.length === 0,
        message: hit.length ? `承诺式表述(无排班 API,不能定): ${hit.join(',')}` : undefined,
      };
    },
  },
  {
    name: 'no_bold_concrete_time',
    severity: 'block',
    check(output) {
      const text = [output.reviewAdvice, output.closing].join('\n');
      const m = text.match(BOLD_TIME_REGEX);
      return { pass: !m, message: m ? `加粗了具体时间"${m[0]}" — 应保留【时间段】占位` : undefined };
    },
  },
  {
    name: 'no_clinical_codes',
    severity: 'block',
    check(output) {
      const m = fullText(output).match(CLINICAL_CODE_REGEX);
      return { pass: !m, message: m ? `出现内部代码/术语"${m[0]}"(患者听不懂)— 改用大白话` : undefined };
    },
  },
  // 注:≤18 岁禁拍片 由 prompt/base 约束(SafetyContext 不带 age,无法在此判定)
];

/** 段数组形态(标准/深度自由段)取全文 */
function sectionsText(o: { sections: ReadonlyArray<{ markdown: string }> }): string {
  return o.sections.map((s) => s.markdown).join('\n');
}

/**
 * 段数组形态(标准档"去模板" = sections[])的机器安全规则 —— 与 4 段版同源
 * (FORBIDDEN/COMMIT/BOLD 同一套常量),只是从 sections[].markdown 取全文。
 * 自由段无法隔离"复查/结束段",承诺/加粗时间全段扫(更严无妨)。
 */
export const SCRIPT_SAFETY_RULES_SECTIONS: ReadonlyArray<
  SafetyRule<{ sections: ReadonlyArray<{ markdown: string }> }>
> = [
  {
    name: 'no_forbidden_phrases',
    severity: 'block',
    check(o) {
      const hit = FORBIDDEN_PHRASES.filter((p) => sectionsText(o).includes(p));
      return { pass: hit.length === 0, message: hit.length ? `命中禁词: ${hit.join(',')}` : undefined };
    },
  },
  {
    name: 'no_commit_phrasing',
    severity: 'block',
    check(o) {
      const hit = COMMIT_PHRASES.filter((p) => sectionsText(o).includes(p));
      return {
        pass: hit.length === 0,
        message: hit.length ? `承诺式表述(无排班 API,不能定): ${hit.join(',')}` : undefined,
      };
    },
  },
  {
    name: 'no_bold_concrete_time',
    severity: 'block',
    check(o) {
      const m = sectionsText(o).match(BOLD_TIME_REGEX);
      return { pass: !m, message: m ? `加粗了具体时间"${m[0]}" — 应保留【时间段】占位` : undefined };
    },
  },
  {
    name: 'no_clinical_codes',
    severity: 'block',
    check(o) {
      const m = sectionsText(o).match(CLINICAL_CODE_REGEX);
      return { pass: !m, message: m ? `出现内部代码/术语"${m[0]}"(患者听不懂)— 改用大白话` : undefined };
    },
  },
];
