import { Injectable, Logger } from '@nestjs/common';
import type { AiCall } from '../../../../ai-call.interface';
import { DraftPlanScriptSchema } from './schema';
import type { DraftPlanScriptInput, DraftPlanScriptOutput } from '../../shared/input.types';
import { buildDraftPlanScriptPrompt } from './prompt';
import { composeSystem } from '../../shared/skill-composer';
import { DraftPlanScriptSkillRegistry } from '../../shared/skill-registry.service';
import { smartDateDisplay } from '../../shared/script-facts';
import { resolveDisease } from './phrasing';
import { deidentifyDoctor } from '../../shared/pii';
import { SCRIPT_SAFETY_RULES } from '../../shared/safety-rules';

// 安全规则(禁词/承诺/加粗时间)走单一源 shared/safety-rules.ts,三档共用,不再各档内联。
const safetyRules = SCRIPT_SAFETY_RULES;

/**
 * 降级 fallback —— LLM 失败 / safety 拒收时用。
 * 用 input 直接拼一份 4 段 markdown 模板话术,保证客服一定有东西可用。
 *
 * ⚠️ fallback 文本本身也要过 safety rule(no_bold_concrete_time / no_commit_phrasing):
 *   不加粗具体时间、不承诺、时间一律用【时间段】/【具体预约时间】占位。
 */
export function stableTemplateFallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
  const { patient, clinicName, plan, clinicalContext } = input;
  const salutation = patient.salutation; // 统一通话称呼(年龄+性别+监护人 aware,orchestrator 算好)
  // 漏诊项 = 聚焦项 reasons[0](orchestrator 已 priorityScore 降序,单一源)→ 转换层归一
  const topReason = (plan.reasons ?? [])[0];
  // 去名:韩维 → 韩(下面拼"韩医生");跟 prompt 同口径
  const doctor = deidentifyDoctor(topReason?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? null);
  // 开场"自从…来过"锚【最近一次就诊】(可能晚于诊断日),无则退回诊断日/泛指
  const dateBasis =
    clinicalContext.daysSinceLastVisit != null
      ? new Date(Date.now() - clinicalContext.daysSinceLastVisit * 86400_000)
      : topReason?.triggerDate
        ? new Date(topReason.triggerDate)
        : null;
  const dateDisplay = smartDateDisplay(dateBasis, new Date()) ?? '上次';
  const disease = resolveDisease(topReason ?? null, plan.primaryScenarioLabel);
  const risk = disease.risks[0] ?? '这个问题如果一直拖着,后面处理可能更复杂';
  const adv = disease.advantages[0] ?? '趁现在早点处理会更省心';
  const reviewDuration = disease.reviewDuration;

  return {
    tone: 'warm',
    opening: `• ${salutation}您好,我是${clinicName}的客服
• ${doctor}医生特意交代我来关注您的后续情况
• 自从${dateDisplay}来过之后,您口腔情况怎么样?`,
    informMissed: `• 之前${doctor}医生检查时,注意到您有${disease.label}的情况
• ${risk}
• ${adv}
• 这个${doctor}医生也特别嘱咐我们提醒您一下`,
    reviewAdvice: `• 最近方便的话,来院复查一下
• 让${doctor}医生帮您再仔细看看
• ${reviewDuration}
• ${doctor}医生【时间段1】和【时间段2】这两个时间段有空,您看哪个方便?`,
    closing: `【预约成功】
• 好的,那我们【具体预约时间】见,祝您生活愉快
【预约不成功】
• 没关系,您先考虑一下,后续我再跟您联系,祝您生活愉快`,
  };
}

/**
 * promptVersion(base 版本;具体装配的 skill 组合见 agent_invocations.input_snapshot.skills_used 的 composeHash)。
 * 改 system/prompt 文本 → bump 字母;改 schema → bump 日期。
 */
const DRAFT_PLAN_SCRIPT_PROMPT_VERSION =
  'draft_plan_script@2026-06-08-4module-v30'; // v30: 低龄约束(<18 不主推种植,口径同召回 IMPLANT_LAST_AGE)+ 替牙期(≤12)矫治措辞统一说「早期矫治」(同 EARLY_ORTHO_MAX_AGE)。v29: 高龄硬约束(≥75 缺牙禁推种植/手术,落活动义齿;口径同召回目标标签 ELDERLY_PROSTHESIS_AGE)。v28: 自报家门去人名 → 占位【回访客服】(生成期占位/渲染期按登录人回填;修「话术缓存 per-plan + 召回池共享 → 后开的客服读到别人名字」线上 bug);input 去 agent 字段,同 plan 不同客服共享 AI 缓存。v27: schema 去硬长度约束(.min/.max → describe 软引导,修 qwen too_small 必失败);v26: 加画像精简版(⚠禁忌/治疗敏感/特别关注 + rfm/生命周期定语气;切入点留深度档,不诱导推销);v24: 治疗计划补 plannedTreatments(treatment_record planned 结构化;原只读常空的 emr.treatment_plan → 话术缺治疗计划);v23: common.md + 人群共性 SKILL 去 brace(原 {应治未治项}/{诊断医生}/{智能称呼} 改朴素措辞;稳健自身句位模板的 {} 不变,填空机制照常,行为基本不变);v22: 新老客改'熟络度'(recency为主+次数为辅,去二分标签,交LLM);v21: 开场日期改锚【最近一次就诊】(原误用诊断日,患者后来又来过会错位)+'来过之后'/告知'之前那次'区分;v20: schema describe 收口(去与模板矛盾的开场顺序/'下周'/负面例,只留段用途+关键约束)+ prompt/兜底 软化'下周'+ 修陈旧注释;v19: stable format.md 精简(去 common/机器闸重复、自查砍到高风险3条)+ 成人模板优化(去重/换标题/软化结束语);v18: base-common 精简合并去重 + 软化治疗方案口径(可点名/不报价不定细化/落点复查);v17: 目录重组(shared/+tiers/stable/)— base 拆 common+format、人群拆共性知识+稳健句位、病种文案归 stable phrasing、安全单一源 safety-rules、composer tier-aware;修开场顺序冲突。v16: 儿童模板复查段修复(删写死"3个月常规涂氟检查"→对齐本次{应治未治项}+用{复查时长},涂氟降级顺带);child SKILL 1.4.0;v15: user prompt 加"医生那次交代"(医嘱/建议/治疗计划,来自聚焦病历,仅引用不演绎);medicalRecord 补 recommendations;v14: user prompt 加 {牙位}(FDI→俗称)+ 本次目标(plan.goal)+ ≤18 禁拍片 belt;占位收口 {牙位}(删 【缺失牙位】);adult/child 模板带牙位;v13: 撤 token,人名去名留称呼(徐女士/韩医生 直接给,非 token);开场白先称呼确认对方再自报家门;v12: user prompt 人名脱敏(称呼/诊断医生/客服 用 token,生成后回填;监护人全名不进 prompt);v11: 统一通话称呼(年龄+性别+监护人,修"9岁张先生");监护人触达提示;医生标签 最后一次就诊→诊断医生;v10: 病种知识走 disease-knowledge 单一访问源(subKey 优先+文本兜底),修 颌骨囊肿 拿不到风险/优势的 bug;v9: 自报家门用登录客服 岗位+姓名(agent);v8: 占位符统一({}=替换、【】=原样保留);v7: 清除 user prompt 污染;v6: 清 system 污染;v5: 还原原模板

@Injectable()
export class DraftPlanScriptCall
  implements AiCall<DraftPlanScriptInput, DraftPlanScriptOutput>
{
  private readonly logger = new Logger(DraftPlanScriptCall.name);
  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;

  constructor(private readonly skillRegistry: DraftPlanScriptSkillRegistry) {}

  buildPrompt(input: DraftPlanScriptInput) {
    // composer 装配 system(base-system.md + 命中的 skill 正文);user prompt 按患者拼事实
    const composed = composeSystem(input, this.skillRegistry.getAllSkills(), 'stable');
    if (composed.matchedSkills.length === 0) {
      this.logger.warn(
        `compose 0 个 skill 命中(scenario=${composed.context.scenario}, ` +
          `dx=${composed.context.diagnosisCodes.join(',')}, ` +
          `pop=${composed.context.population}, rel=${composed.context.relationship}) — ` +
          `system 回退仅 base 部分,可能效果下降`,
      );
    } else {
      this.logger.debug(
        `compose skills: ${composed.matchedSkills.map((s) => s.frontmatter.name).join(', ')} ` +
          `(hash=${composed.composeHash})`,
      );
    }
    return {
      system: composed.systemPrompt,
      prompt: buildDraftPlanScriptPrompt(input),
    };
  }

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