import { Injectable } from '@nestjs/common';
import type { AiCall } from '../../ai-call.interface';
import type { SafetyRule } from '../../core/safety-gate.service';
import { DraftPlanSummarySchema } from './schema';
import type { DraftPlanSummaryInput, DraftPlanSummaryOutput } from './input.types';
import {
  DRAFT_PLAN_SUMMARY_PROMPT_VERSION,
  DRAFT_PLAN_SUMMARY_SYSTEM,
  buildDraftPlanSummaryPrompt,
} from './prompt';

/**
 * Safety:跟 script 一套禁词检查,但 summary 偶尔需要客观描述"患者拒绝"等,
 * 所以禁词列表更保守,避免误杀。
 */
const FORBIDDEN_PHRASES = ['一定能', '保证', '绝对', '百分百', '100%', '亲爱的'];

const safetyRules: ReadonlyArray<SafetyRule<DraftPlanSummaryOutput>> = [
  {
    name: 'no_forbidden_phrases',
    severity: 'block',
    check(output) {
      const fullText = [output.onePage, output.medicalRecord, output.treatmentChain].join('\n');
      const hit = FORBIDDEN_PHRASES.filter((p) => fullText.includes(p));
      return {
        pass: hit.length === 0,
        message: hit.length > 0 ? `命中禁词: ${hit.join(',')}` : undefined,
      };
    },
  },
];

/**
 * Fallback — LLM 失败时用 input 拼最朴素的版本。
 * 比 script 简单:facts/chains 直接列出来,客服自己看。
 */
function fallback(input: DraftPlanSummaryInput): DraftPlanSummaryOutput {
  const { patient, plan, facts, chains, clinicName } = input;
  const reasonText = plan.reasons[0]?.reason ?? plan.primaryScenarioLabel;

  const onePage = `**${patient.nameMasked}**(${patient.gender ?? '?'}·${patient.age ?? '?'}岁) — 来自 ${clinicName}\n\n本次召回原因:${reasonText}\n\nAI 总结暂未生成,请参考下方"病历"与"治疗链"两段。`;

  const factGroups: Record<string, typeof facts> = {};
  for (const f of facts) {
    if (!factGroups[f.type]) factGroups[f.type] = [];
    factGroups[f.type]!.push(f);
  }
  const medicalRecord =
    Object.entries(factGroups)
      .map(([type, fs]) => {
        const lines = fs
          .slice(0, 6)
          .map((f) => `- ${f.occurredAt?.slice(0, 10) ?? '?'} ${f.title ?? f.summary ?? type}`)
          .join('\n');
        return `### ${type}\n${lines}`;
      })
      .join('\n\n') || '(无病历数据)';

  const treatmentChain =
    chains.length > 0
      ? chains
          .map(
            (c) =>
              `### ${c.name} [${c.status}]\n当前阶段 ${c.currentStage};共 ${c.nodes.length} 个节点。`,
          )
          .join('\n\n')
      : '(无治疗链数据)';

  return { onePage, medicalRecord, treatmentChain };
}

@Injectable()
export class DraftPlanSummaryCall
  implements AiCall<DraftPlanSummaryInput, DraftPlanSummaryOutput>
{
  readonly kind = 'summary' as const;
  readonly callKey = 'draft_plan_summary';
  readonly promptVersion = DRAFT_PLAN_SUMMARY_PROMPT_VERSION;
  /**
   * 🔴 2026-08-15 由 `deepseek-v4-flash` 改成裸键 `'qwen'`（产品定：摘要类统一同一个模型）。
   *
   * 此前它是**四个摘要里唯一的例外** —— 另外三个（召回简报 / 画像小结 / 召回小结）
   * 都写着「这类一句话摘要统一走 Qwen」，只有它留在 DeepSeek 上，于是同一类活
   * 跑着两个模型、两套价、两种风格，而**没有任何地方记着为什么**。
   * ⚠️ 用裸键 `'qwen'` 而不是钉版本：跟另外三个一致，跟随 `QWEN_DEFAULT_MODEL`。
   *   （话术那个下拉是**型号选择器**，那里钉具体版本是刻意的，⛔ 别拿来类比。）
   * ⚠️ 换过来顺带关掉了思考：qwen 那条 fetch 中间件对**无 tools** 的调用注入
   *   `enable_thinking:false`，而摘要走结构化输出、没有 tools。
   */
  readonly defaultModelId = 'qwen';
  readonly outputSchema = DraftPlanSummarySchema;
  readonly safetyRules = safetyRules;

  buildPrompt(input: DraftPlanSummaryInput) {
    return {
      system: DRAFT_PLAN_SUMMARY_SYSTEM,
      prompt: buildDraftPlanSummaryPrompt(input),
    };
  }

  fallback(input: DraftPlanSummaryInput): DraftPlanSummaryOutput {
    return fallback(input);
  }
}
