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 步骤事件,最后落库并 yield done。
   *
   * ⭐ 事件形状与电话深度档一致(`{type:'step'|'done'}`),前端整套复用:
   *   ScriptDeepProcess 时间线、停止按钮、AIStamp。⛔ 不为企微另造一套交互。
   */
  async *generateStream(
    planId: string,
    options: { modelIdOverride?: string; signal?: AbortSignal } = {},
  ): AsyncGenerator<
    | { type: 'step'; step: string; status: 'running' | 'done'; detail?: unknown }
    | { 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 === 'step') {
        yield { type: 'step', step: ev.step, status: ev.status, detail: ev.detail };
      } 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; stepsRun: string[]; 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 ?? '未知'};步骤=${r.stepsRun.join('→')}`,
      );
    }
    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 } : {}),
    };
  }
}
