import { Injectable, Logger } 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_LEGACY,
  DRAFT_PLAN_SCRIPT_SYSTEM_LEGACY,
  buildDraftPlanScriptPrompt,
} from './prompt';
import { composeSystem } from './skill-composer';
import { DraftPlanScriptSkillRegistry } from './skill-registry.service';
import {
  resolveAgeBranch,
  resolveSalutation,
  smartDateDisplay,
  missedFromReason,
  lookupKeyPoints,
  lookupReviewDuration,
} from './script-facts';

/**
 * 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.informMissed, output.reviewAdvice, output.closing].join('\n');
      const hit = FORBIDDEN_PHRASES.filter((p) => fullText.includes(p));
      return {
        pass: hit.length === 0,
        message: hit.length > 0 ? `命中禁词: ${hit.join(',')}` : undefined,
      };
    },
  },
  {
    name: 'no_commit_phrasing',
    severity: 'block',
    check(output) {
      // 结束/复查段禁止"我已为您约好 X" 这种确定承诺(PAC 无排班 API,时间用【占位】不写死)
      const COMMIT_PHRASES = [
        '已为您约好', '已成功预约', '已为您预约', '已经为您约', '已替您预约',
        '约定本', '敲定本', '安排好了', '已经预约',
      ];
      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 > 0 ? `承诺式表述(无排班 API,不能定): ${hit.join(',')}` : undefined,
      };
    },
  },
  {
    name: 'no_bold_concrete_time',
    severity: 'block',
    check(output) {
      // 禁止 **周X上午X点** 加粗具体时间(误导"已定");新结构应保留【时间段1】【具体预约时间】占位
      const boldTimeRegex = /\*\*[^*\n]*(?:周[一二三四五六日天]|\d+\s*(?:点|:|：))[^*\n]*\*\*/;
      const text = [output.reviewAdvice, output.closing].join('\n');
      const m = text.match(boldTimeRegex);
      return { pass: !m, message: m ? `加粗了具体时间"${m[0]}" — 应保留【时间段】占位` : undefined };
    },
  },
  // 注:≤18 岁禁拍片 由 prompt/base-system 约束(SafetyContext 不带 age,无法在此判定)
];

/**
 * 降级 fallback —— LLM 失败 / safety 拒收时用。
 * 用 input 直接拼一份 4 段 markdown 模板话术,保证客服一定有东西可用。
 *
 * ⚠️ fallback 文本本身也要过 safety rule(close_no_bold_time / close_has_tentative_phrasing)。
 * 历史踩坑:close 段写 `**本周六上午 10 点**` 加粗时间,自己触发 close_no_bold_time block。
 * 已改:不加粗 + (示例) 后缀 + 显式"以诊所排班为准"。
 */
function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
  const { patient, clinicName, plan, clinicalContext } = input;
  const branch = resolveAgeBranch(patient.age);
  const salutation = resolveSalutation({ nameMasked: patient.nameMasked, gender: patient.gender, branch });
  const doctor = clinicalContext.primaryDoctorName ?? '您的主诊医生';
  const dateDisplay =
    smartDateDisplay(
      clinicalContext.daysSinceLastVisit != null
        ? new Date(Date.now() - clinicalContext.daysSinceLastVisit * 86400_000)
        : null,
      new Date(),
    ) ?? '上次';
  // 漏诊项 = PAC 应治未治 reason(取 priorityScore 最高的一条)→ 转换层归一
  const topReason = [...(plan.reasons ?? [])].sort((a, b) => b.priorityScore - a.priorityScore)[0];
  const missed = topReason
    ? missedFromReason(topReason)
    : { label: plan.primaryScenarioLabel, key: null };
  const kp = lookupKeyPoints(missed.key);
  const risk = kp?.risks[0] ?? '这个问题如果一直拖着,后面处理可能更复杂';
  const adv = kp?.advantages[0] ?? '趁现在早点处理会更省心';
  const reviewDuration = lookupReviewDuration(missed.key);

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

/**
 * 是否启用 Skills 系统(env 开关,出问题秒回滚)。
 * 默认 1 启用;设 0 退回 legacy 单 prompt 路径。
 */
function isSkillsEnabled(): boolean {
  return (process.env.AI_SCRIPT_USE_SKILLS ?? '1') !== '0';
}

/**
 * Skills 模式 promptVersion(base 版本,跟 legacy 区分;
 * composeHash 可以在 agent_invocations.input_snapshot.skills_used 看到具体装配)。
 */
const DRAFT_PLAN_SCRIPT_PROMPT_VERSION_SKILLS =
  'draft_plan_script@2026-05-31-skills-base-v2'; // v2: user prompt 去掉冗余的 skills 名字索引

@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';
  // ⚠️ promptVersion 选 skills 版还是 legacy 版,取决于 env;
  //   两版输出差异会让 agent_invocations.promptVersion 区分开,SQL 对比效果时拆分群体
  readonly promptVersion = isSkillsEnabled()
    ? DRAFT_PLAN_SCRIPT_PROMPT_VERSION_SKILLS
    : DRAFT_PLAN_SCRIPT_PROMPT_VERSION_LEGACY;
  readonly defaultModelId = 'deepseek-v4-flash';
  readonly outputSchema = DraftPlanScriptSchema;
  readonly safetyRules = safetyRules;

  constructor(private readonly skillRegistry: DraftPlanScriptSkillRegistry) {}

  buildPrompt(input: DraftPlanScriptInput) {
    if (!isSkillsEnabled()) {
      // legacy 路径 — 老 prompt 单系统词
      return {
        system: DRAFT_PLAN_SCRIPT_SYSTEM_LEGACY,
        prompt: buildDraftPlanScriptPrompt(input),
      };
    }

    // skills 路径 — composer 装配 system(含 skill 正文);user prompt 不再重列 skills 名字
    const composed = composeSystem(input, this.skillRegistry.getAllSkills());
    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 fallback(input);
  }
}
