import { Injectable } from '@nestjs/common';
import type { AiCall } from '../../ai-call.interface';
import type { SafetyRule } from '../../core/safety-gate.service';
import { DraftPlanScriptSchema } from './schema';
import type { DraftPlanScriptInput, DraftPlanScriptOutput } from './input.types';
import {
  DRAFT_PLAN_SCRIPT_PROMPT_VERSION,
  DRAFT_PLAN_SCRIPT_SYSTEM,
  buildDraftPlanScriptPrompt,
} from './prompt';

/**
 * Safety rules — 后置硬约束。
 * LLM 偶尔会越过 schema 约束塞禁词,这里再补一道(防御性)。
 * B 方案重写后,output 4 段都是 markdown 字符串,直接全文 join 扫禁词。
 */
const FORBIDDEN_PHRASES = [
  '一定能', '保证', '绝对', '百分百', '100%',
  '亲爱的', '亲', '宝',
  '便宜', '促销', '折扣', '免费送',
];

const safetyRules: ReadonlyArray<SafetyRule<DraftPlanScriptOutput>> = [
  {
    name: 'no_forbidden_phrases',
    severity: 'block',
    check(output) {
      const fullText = [output.opening, output.followup, output.objection, output.close].join('\n');
      const hit = FORBIDDEN_PHRASES.filter((p) => fullText.includes(p));
      return {
        pass: hit.length === 0,
        message: hit.length > 0 ? `命中禁词: ${hit.join(',')}` : undefined,
      };
    },
  },
  {
    name: 'close_has_concrete_time',
    severity: 'warn', // warn 不阻断,只记日志
    check(output) {
      // 启发式:结束段 markdown 必须含数字(具体时间点 / 日期)
      const hasDigit = /\d/.test(output.close);
      return { pass: hasDigit, message: hasDigit ? undefined : 'close 段未含具体时间数字' };
    },
  },
  {
    name: 'objection_uses_h3_blocks',
    severity: 'warn',
    check(output) {
      // 启发式:objection 段必须按 ### A./B./C. 子标题切分
      const hasH3 = /^###\s+[A-D]\./m.test(output.objection);
      return { pass: hasH3, message: hasH3 ? undefined : 'objection 段未按 ### A./B. 子标题切分' };
    },
  },
];

/**
 * 降级 fallback —— LLM 失败 / safety 拒收时用。
 * 用 input 直接拼一份 4 段 markdown 模板话术,保证客服一定有东西可用。
 */
function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
  const { patient, clinicName, plan, clinicalContext } = input;
  const dayPart = clinicalContext.daysSinceLastVisit
    ? `已经 ${clinicalContext.daysSinceLastVisit} 天没见您了`
    : '有段时间没见您了';
  const lastSummary = clinicalContext.lastVisitSummary ?? '复查相关安排';

  return {
    tone: 'warm',
    opening: `**目的**:亲切自然地建立通话,围绕「${plan.primaryScenarioLabel}」开场,避免推销感。

> "${patient.nameMasked}您好,我是${clinicName}的客服。${dayPart},今天给您打电话,主要是想跟您同步上次的${lastSummary},您现在方便聊几分钟吗?"

**注意**
- 称呼用「${patient.nameMasked}」,不用全名
- 若不方便,主动询问合适回拨时间`,
    followup: `**目的**:把诊所记录的临床事实,自然引到「该回来做点什么」。

> "根据病历记录,本次想跟您沟通的是:${plan.primaryScenarioLabel}。复查不需要太久,大概 20 分钟就能完成。"

> "我们这边可以帮您安排医生面诊评估,**这次只是评估和确认方案,不做任何操作**,您看本周末或下周初哪个时间方便?"

**异议预判**
- "最近没时间" → 提供更灵活的时间窗(早班/晚班)
- "再考虑一下" → 强调诊后复查窗口期,过期可能要重新评估
- "已在外院看过" → 提交「已在外院治疗」并关闭召回`,
    objection: `### A. "我再考虑考虑"
> "完全理解。这样,我先帮您把医生的面诊时间留出来,**本周六上午 10 点或下周一晚上 7 点**,您选一个?到现场看了方案再决定也不晚。"

### B. "最近真的没时间"
> "理解,可以约到下个月,提前预约能避开排队。您下周或下下周哪天比较方便?我先帮您预留。"

### C. "已在别的医院看了"
> "好的${patient.nameMasked},那我这边帮您把这条记录关一下,日常护理还是按原来的周期回来就行,**祝您一切顺利**。"
> → 提交结果选「已在外院治疗」`,
    close: `> "好的${patient.nameMasked},那我帮您约 **本周六上午 10 点**,到时候提前 10 分钟到前台就行。我会给您发个短信提醒,您注意接收。还有别的需要么?"

**回写要点**
- 成功约上面诊 → 提交结果选「成功转化为新预约」,填预约时间 + 医生
- 同意但未定日期 → 选「约定下次回访」,填预计时间
- 考虑中 → 选「考虑中近期再跟进」,7 天后系统提醒二次跟进`,
  };
}

@Injectable()
export class DraftPlanScriptCall
  implements AiCall<DraftPlanScriptInput, DraftPlanScriptOutput>
{
  readonly kind = 'script' as const;
  readonly callKey = 'draft_plan_script';
  readonly promptVersion = DRAFT_PLAN_SCRIPT_PROMPT_VERSION;
  readonly defaultModelId = 'deepseek-v4-flash';
  readonly outputSchema = DraftPlanScriptSchema;
  readonly safetyRules = safetyRules;

  buildPrompt(input: DraftPlanScriptInput) {
    return {
      system: DRAFT_PLAN_SCRIPT_SYSTEM,
      prompt: buildDraftPlanScriptPrompt(input),
    };
  }

  fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
    return fallback(input);
  }
}
