import { Injectable } from '@nestjs/common';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import type { AiCall } from '../../ai-call.interface';
import type { ScriptContext } from '../draft-plan-script/shared/input.types';
import { composeSystem } from '../draft-plan-script/shared/skill-composer';
import { DraftPlanScriptSkillRegistry } from '../draft-plan-script/shared/skill-registry.service';
import {
  WecomPlanSchema,
  WecomWriteSchema,
  WecomVerifySchema,
  type WecomPlanZ,
  type WecomWriteZ,
  type WecomVerifyZ,
} from './schema';
import { buildWecomPlanPrompt, buildWecomWritePrompt, buildWecomVerifyPrompt } from './prompts';

/**
 * 企微话术的 3 个 AiCall(与电话深度档同构:plan → write → verify,各自落 agent_invocations)。
 *
 * ═══ 与电话档共用了什么、没共用什么 ═══════════════════════════
 * 共用(直接 import,⛔ 不复制):
 *   · ScriptContext 输入契约、`buildRichFactBlock` 患者事实块(谁、哪颗牙、医生说了什么)
 *   · 安全护栏 `forbiddenWordsBlock` / 福利硬约束 / 自报家门占位 / 医生姓脱敏
 *   · 人群 skills(成人/儿童共性)与 `composeSystem` 装配逻辑
 *   —— 这些都跟"用电话还是企微说"无关。复制一份的代价是护栏有两处,
 *      改了一处另一处会悄悄留在旧版本,而漏了哪条要等客服**已经发给患者**才发现。
 * 没共用(本目录自己写):
 *   · 输出 schema:电话 `sections[]`(伴飞逐段高亮要它)→ 企微单块 `markdown`
 *   · format.md:电话是口语/分段/口头二选一 → 企微是书面/断行/可复制即发
 *   · verify 多一条⑤「可直接发送」(无小标题、无占位残留、无给客服看的话)
 *
 * ⚠️ 企微**只有深度档**(产品定):它是一条发出去就收不回的消息,
 *   没有"边打边看着调整"的机会,所以不给低质量档。
 */

/**
 * 本目录自己的输出格式(覆盖 tier 默认的那份)。
 *
 * ⛔ **不能用 `__dirname`**(踩过:ENOENT)—— SWC dev 的产物在 `dist/src/...`、
 *   tsc prod 在 `dist/...`,同一个 `__dirname` 在两态下指向不同层级。
 *   照抄 `resolveScriptRoot` 那套 env → src → dist 的策略(理由见它的文件头注释):
 *   `cwd` 在 dev/prod 都是 apps/pac-service 根,才是稳的。
 */
function wecomFormatPath(): string {
  const rel = 'modules/ai/calls/draft-wecom-script/skills/_base/format.md';
  const src = join(process.cwd(), 'src', rel);
  return existsSync(src) ? src : join(process.cwd(), 'dist', rel);
}

const PLAN_SYSTEM = [
  '你是资深口腔回访话术规划师。基于给定的患者事实,规划一条**发到患者微信里**的医疗关怀消息:要讲哪几点、按什么顺序讲。',
  '原则:医疗关怀非销售;以本次聚焦项(应治未治)为主线;每个要点都必须能追到给定事实,不编造。',
  '',
  '# 这是微信,不是电话',
  '患者会**一口气读完**,没有一来一回。所以你排的是「讲的先后」,**不是分段** ——',
  '最终产出是一整段连贯文字,不会有小标题。别按"开场/正文/结尾"去想,想的是"先说什么才能让下一句站得住"。',
  '',
  '# 规划方法:从果(目标)倒推到因',
  '先定这条消息的"果" —— 让患者明白"该回来把 X 处理掉、早处理的好处 / 拖着的后果",从而愿意来复查;',
  '再倒推"因" —— 为达成它,患者需要先知道什么、被打消什么顾虑。据此把链条拆成层层递进的要点。',
  '',
  '# 说明风险的分寸:把后果讲清,但不吓唬、不推销',
  '"不处理会怎样"要**客观说明**(结合病历 + 牙科常识),让患者理解严重性;',
  '但**不夸大、不制造恐慌、不下吓人结论**,也**不报价、不促单、不施压**。',
  '',
  '不要写正文,只输出要点 JSON。要点 3-6 条。',
].join('\n');

const VERIFY_SYSTEM = [
  '你是严格的医疗话术审核员,任务是**对抗式校验**一条准备**发到患者微信**的消息,默认怀疑、宁严勿松。',
  '依据给定的"本次回访患者信息"逐项判断:',
  '① 接地:每个具体说法(诊断/检查所见/医嘱/时间/牙位/医生)能否在给定事实里找到依据?找不到=编造。',
  '② 安全:有无报价/费用、疗效承诺、患者≤18 却提拍片?',
  '   ⛔ **出现任何时间占位符(【时间段1】【时间段2】【具体预约时间】)= 不合格** ——',
  '   企微是整段复制直接发出去的,占位符会原样发给患者;而客服要先手动改一遍的话,',
  '   "可直接发送"就不成立了。⛔ 自己编具体时间("周三上午")同样不合格。',
  '   正确写法是不含具体时间的邀约(「您方便的时候回我一下,我帮您安排」)。',
  '③ 逻辑与分寸:围绕"让患者明白该回来处理"展开、层层递进、该说的没缺(点了问题要说后果、说了后果要给出路);',
  '   有没有**吓唬/制造恐慌**或**推销/促单/施压**口吻?',
  '④ 患者听得懂:有没有诊断代码(如 K08)、英文/内部枚举、生硬术语,或含糊其辞(没说清哪颗牙)?',
  '⑤ ⭐**可直接发送**(企微专有):客服会整段复制发出去 ——',
  '   有小标题 / `##` / 分段编号 / "第一第二" = 不合格;',
  '   混进给客服自己看的话("以下话术供参考""建议这样说") = 不合格;',
  '   ⛔ 除自报家门里的 【回访客服】 外**残留任何 `【】` 占位或内部标签 = 不合格**;',
  '   出现"您现在方便吗""能听清吗"这类**需要对方当场回话**才成立的电话句式 = 不合格。',
  '①②③④⑤ 任一不过 → pass=false,并逐条列出 issue(位置、问题、修法);全部通过 → pass=true、issues 空。',
  '另外给一组 **quality 质量评分**(1-5)—— **只评"好不好",跟 pass 无关**。',
  '只输出 JSON,不改写草稿。',
].join('\n');

@Injectable()
export class WecomPlanCall implements AiCall<ScriptContext, WecomPlanZ> {
  readonly kind = 'script' as const;
  readonly callKey = 'draft_wecom_script_plan';
  readonly promptVersion = 'draft_wecom_script@2026-08-04-plan-v1';
  readonly defaultModelId = 'deepseek-v4-flash';
  readonly outputSchema = WecomPlanSchema;
  buildPrompt(ctx: ScriptContext) {
    return { system: PLAN_SYSTEM, prompt: buildWecomPlanPrompt(ctx) };
  }
}

export interface WecomWriteInput {
  ctx: ScriptContext;
  plan: WecomPlanZ;
  prevDraft?: WecomWriteZ;
  repairIssues?: Array<{ section: string; problem: string; fix: string }>;
}

@Injectable()
export class WecomWriteCall implements AiCall<WecomWriteInput, WecomWriteZ> {
  readonly kind = 'script' as const;
  readonly callKey = 'draft_wecom_script_write';
  readonly promptVersion = 'draft_wecom_script@2026-08-04-write-v1';
  readonly defaultModelId = 'deepseek-v4-flash';
  readonly outputSchema = WecomWriteSchema;
  constructor(private readonly skillRegistry: DraftPlanScriptSkillRegistry) {}
  buildPrompt(input: WecomWriteInput) {
    // tier 传 'deep' 只为**挑 skill**(人群共性 / 深度档知识,与渠道无关);
    // 输出形态靠第 4 个参数换成企微那份 format.md。见 composeSystem 的注释。
    const composed = composeSystem(
      input.ctx,
      this.skillRegistry.getAllSkills(),
      'deep',
      wecomFormatPath(),
      false, // ⛔ 企微不留【时间段】占位(整段复制直发,占位会原样发给患者)
    );
    return { system: composed.systemPrompt, prompt: buildWecomWritePrompt(input) };
  }
}

export interface WecomVerifyInput {
  ctx: ScriptContext;
  draft: WecomWriteZ;
}

@Injectable()
export class WecomVerifyCall implements AiCall<WecomVerifyInput, WecomVerifyZ> {
  readonly kind = 'judge' as const;
  readonly callKey = 'draft_wecom_script_verify';
  readonly promptVersion = 'draft_wecom_script@2026-08-04-verify-v1';
  readonly defaultModelId = 'deepseek-v4-flash';
  readonly outputSchema = WecomVerifySchema;
  buildPrompt(input: WecomVerifyInput) {
    return { system: VERIFY_SYSTEM, prompt: buildWecomVerifyPrompt(input) };
  }
}
