import { Injectable, Logger } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { PrismaService } from '../../../prisma/prisma.service';
import { PlanScriptOrchestrator } from './plan-script.orchestrator';
import { WecomScriptStrategy } from '../calls/draft-wecom-script/wecom.strategy';

export interface WecomScriptGenerateResult {
  planId: string;
  planScriptId: string | null;
  agentInvocationId: string;
  source: 'agent' | 'failed';
  content: string;
  costYuan: number;
  failReason?: string;
}

/**
 * 企微话术编排 —— 读 plan/persona/facts → 跑 3 步 → 写 `plan_scripts(channel='wecom')`。
 *
 * ⭐ **上下文装配整段复用电话档的 `buildScriptInputForPlan`**:
 *   患者是谁、哪颗牙、医生说了什么、上次什么时候来的、福利是什么 —— 与渠道无关。
 *   ⛔ 别在这里另写一遍取数:那意味着两条链路对"患者事实"各有一套理解,
 *   而它们一定会漂(一边加了字段另一边没加,表现是企微稿比电话稿少提一颗牙,还不报错)。
 *
 * ⚠️ 与电话档另一处不同:**失败不写 `ready`**。
 *   电话档失败会落模板兜底稿(客服拿着电话必须有东西念);企微稿是要**原样发给患者**的,
 *   落一份套话让他复制发出去比没有更糟 —— 所以失败就写 `failed`,前端显示"生成失败,请手写"。
 */
@Injectable()
export class WecomScriptOrchestrator {
  private readonly logger = new Logger(WecomScriptOrchestrator.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly planScripts: PlanScriptOrchestrator,
    private readonly strategy: WecomScriptStrategy,
  ) {}

  /**
   * 流式生成 —— **推正文增量**,最后落库并 yield done。
   *
   * ⚠️ 2026-08 改动:原来推的是「步骤」事件(规划/撰写/自检),配一套时间线组件。
   *   工作流砍掉之后没有步骤可推了,改推 `delta`(正文增量)——
   *   体感从"盯着三个勾等 60 秒"变成"看着它写"。
   * ⚠️ 推的是**增量**不是全文:前端直接追加即可,不必每帧重排整段。
   * ⚠️ 机器安全闸在 `done` 时判。不过则 `source='failed'` 且 `content` 为空 ——
   *   前端必须据此**清掉已经流出来的正文**,⛔ 别把没过闸的稿子留在框里让人复制。
   */
  async *generateStream(
    planId: string,
    options: { modelIdOverride?: string; signal?: AbortSignal } = {},
  ): AsyncGenerator<
    | { type: 'delta'; text: string }
    | { type: 'done'; content: string; source: 'agent' | 'failed'; invocationId: string; failReason?: string }
  > {
    const { ctx, plan } = await this.load(planId);
    const runCtx = this.runCtx(plan, options);
    let result: WecomScriptGenerateResult | null = null;

    for await (const ev of this.strategy.runStream(ctx, runCtx)) {
      if (ev.kind === 'delta') {
        yield { type: 'delta', text: ev.text };
      } else {
        result = await this.persist(plan, ev.result);
      }
    }
    yield {
      type: 'done',
      content: result?.content ?? '',
      source: result?.source ?? 'failed',
      invocationId: result?.agentInvocationId ?? '',
      ...(result?.failReason ? { failReason: result.failReason } : {}),
    };
  }

  async generate(
    planId: string,
    options: { modelIdOverride?: string; signal?: AbortSignal } = {},
  ): Promise<WecomScriptGenerateResult> {
    const { ctx, plan } = await this.load(planId);
    const r = await this.strategy.run(ctx, this.runCtx(plan, options));
    return this.persist(plan, r);
  }

  /**
   * ⭐ 上下文装配整段复用电话档的 `buildScriptInputForPlan`(见类注释)。
   * ⛔ 别在这里另写一遍取数:两条链路对"患者事实"各有一套理解就一定会漂
   * (一边加了字段另一边没加,表现是企微稿比电话稿少提一颗牙,还不报错)。
   */
  private async load(planId: string) {
    const ctx = await this.planScripts.buildScriptInputForPlan(planId);
    const plan = await this.prisma.followupPlan.findUniqueOrThrow({
      where: { id: planId },
      select: { id: true, hostId: true, tenantId: true, patientId: true },
    });
    return { ctx, plan };
  }

  private runCtx(
    plan: { id: string; hostId: string; tenantId: string; patientId: string },
    options: { modelIdOverride?: string; signal?: AbortSignal },
  ) {
    return {
      hostId: plan.hostId,
      tenantId: plan.tenantId,
      linkedPatientId: plan.patientId,
      linkedPlanId: plan.id,
      // 一次生成的 3 步(plan/write/verify)用同一个 runId 串起来,eval 里能按次回看
      workflowRunId: randomUUID(),
      bustCache: true,
      modelIdOverride: options.modelIdOverride,
      evalMode: 'production' as const,
      signal: options.signal,
    };
  }

  /**
   * 落库 —— ⚠️ 失败**不写 ready**:企微稿是要原样发给患者的,
   * 落一份套话让他复制发出去比没有更糟。失败就写 failed,前端显示"生成失败,请手写"。
   */
  private async persist(
    plan: { id: string; hostId: string; tenantId: string },
    r: { markdown: string; source: 'agent' | 'failed'; invocationId: string; costYuan: number; failReason?: string },
  ): Promise<WecomScriptGenerateResult> {
    const ok = r.source === 'agent' && r.markdown.trim().length > 0;
    const row = await this.prisma.planScript.upsert({
      where: { planId_channel: { planId: plan.id, channel: 'wecom' } },
      create: {
        hostId: plan.hostId,
        tenantId: plan.tenantId,
        planId: plan.id,
        channel: 'wecom',
        content: ok ? r.markdown : null,
        status: ok ? 'ready' : 'failed',
        source: ok ? 'agent' : null,
        agentInvocationId: r.invocationId || null,
      },
      update: {
        content: ok ? r.markdown : null,
        status: ok ? 'ready' : 'failed',
        source: ok ? 'agent' : null,
        agentInvocationId: r.invocationId || null,
      },
      select: { id: true },
    });
    if (!ok) {
      this.logger.warn(
        `企微话术生成失败 plan=${plan.id}: ${r.failReason ?? '未知'}`,
      );
    }
    return {
      planId: plan.id,
      planScriptId: row.id,
      agentInvocationId: r.invocationId,
      source: r.source,
      content: ok ? r.markdown : '',
      costYuan: r.costYuan,
      ...(r.failReason ? { failReason: r.failReason } : {}),
    };
  }
}
