import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import type { Prisma } from '@prisma/client';
import { calcAge, maskName } from '@pac/utils';
import { planScenarioLabel, personaFeatureMeta } from '@pac/types';
import { PrismaService } from '../../../prisma/prisma.service';
import { ChainComposerService } from '../../plan/engine/chain-composer.service';
import { AiCallRunnerService } from '../ai-call-runner.service';
import { DraftPlanSummaryCall } from '../calls/draft-plan-summary/call';
import type {
  DraftPlanSummaryInput,
  DraftPlanSummaryOutput,
} from '../calls/draft-plan-summary/input.types';

/**
 * PlanSummaryOrchestrator — 业务编排:planId → AI 3 段摘要 → 写 3 行 PlanSummary。
 *
 * 设计要点:
 *   - **1 次 LLM 调用**(token 优化,共享上下文)
 *   - 3 行 PlanSummary 共享同一 agentInvocationId,审计时能 join 回去
 *   - upsert by (planId, type) 唯一约束
 */

const SUMMARY_TYPES = ['one_page', 'medical_record', 'treatment_chain'] as const;
type SummaryType = (typeof SUMMARY_TYPES)[number];

export interface PlanSummaryGenerateOptions {
  bustCache?: boolean;
  modelIdOverride?: string;
  dryRun?: boolean;
}

export interface PlanSummaryGenerateResult {
  planId: string;
  agentInvocationId: string;
  source: 'agent' | 'template_fallback';
  cacheHit: boolean;
  costYuan: number;
  summaries: Array<{
    type: SummaryType;
    planSummaryId: string | null;
    content: string;
  }>;
}

/**
 * SSE 事件 — 跟 plan-script 平行,但 partial 时只 yield 3 段当前态(整段),
 * 不做 section 级渲染(summary 是大段 Markdown,客户端直接 render)。
 */
export type PlanSummaryStreamEvent =
  | { type: 'start'; invocationId: string; modelId: string; promptVersion: string }
  | {
      type: 'partial';
      structured: Partial<DraftPlanSummaryOutput>;
    }
  | {
      type: 'done';
      agentInvocationId: string;
      source: 'agent' | 'template_fallback';
      costYuan: number;
      promptTokens: number;
      completionTokens: number;
      structured: DraftPlanSummaryOutput;
      summaries: Array<{ type: SummaryType; planSummaryId: string | null; content: string }>;
      fallbackReason?: string;
    }
  | { type: 'error'; message: string };

@Injectable()
export class PlanSummaryOrchestrator {
  private readonly logger = new Logger(PlanSummaryOrchestrator.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly runner: AiCallRunnerService,
    private readonly call: DraftPlanSummaryCall,
    private readonly chainComposer: ChainComposerService,
  ) {}

  async generate(
    planId: string,
    options: PlanSummaryGenerateOptions = {},
  ): Promise<PlanSummaryGenerateResult> {
    const ctx = await this.loadPlanContext(planId);
    const input = this.buildCallInput(ctx);

    const workflowRunId = randomUUID();
    const result = await this.runner.run(this.call, input, {
      hostId: ctx.plan.hostId,
      tenantId: ctx.plan.tenantId,
      workflowRunId,
      linkedPatientId: ctx.patient.id,
      linkedPersonaId: ctx.persona?.id,
      linkedPlanId: ctx.plan.id,
      bustCache: options.bustCache ?? false,
      modelIdOverride: options.modelIdOverride,
      evalMode: 'production',
    });

    const summaries = await this.writeSummaries(ctx.plan, result.output, result.invocationId, result.source, options.dryRun);

    return {
      planId: ctx.plan.id,
      agentInvocationId: result.invocationId,
      source: result.source,
      cacheHit: result.cacheHit,
      costYuan: result.costYuan,
      summaries,
    };
  }

  async *generateStream(
    planId: string,
    options: PlanSummaryGenerateOptions = {},
  ): AsyncGenerator<PlanSummaryStreamEvent, void, void> {
    let pctx: PlanContext;
    let input: DraftPlanSummaryInput;
    try {
      pctx = await this.loadPlanContext(planId);
      input = this.buildCallInput(pctx);
    } catch (err) {
      yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
      return;
    }

    const workflowRunId = randomUUID();
    const events = this.runner.stream(this.call, input, {
      hostId: pctx.plan.hostId,
      tenantId: pctx.plan.tenantId,
      workflowRunId,
      linkedPatientId: pctx.patient.id,
      linkedPersonaId: pctx.persona?.id,
      linkedPlanId: pctx.plan.id,
      bustCache: true,
      modelIdOverride: options.modelIdOverride,
      evalMode: 'production',
    });

    let lastStructured: DraftPlanSummaryOutput | null = null;
    let lastInvocationId = '';
    let lastSource: 'agent' | 'template_fallback' = 'agent';
    let lastCost = 0;
    let lastPromptTokens = 0;
    let lastCompletionTokens = 0;
    let lastFallbackReason: string | undefined;

    for await (const evt of events) {
      if (evt.type === 'start') {
        lastInvocationId = evt.invocationId;
        yield { type: 'start', invocationId: evt.invocationId, modelId: evt.modelId, promptVersion: evt.promptVersion };
      } else if (evt.type === 'partial') {
        yield { type: 'partial', structured: evt.partial };
      } else if (evt.type === 'done') {
        lastStructured = evt.output;
        lastSource = evt.source;
        lastCost = evt.costYuan;
        lastPromptTokens = evt.promptTokens;
        lastCompletionTokens = evt.completionTokens;
        lastFallbackReason = evt.fallbackReason;
      } else if (evt.type === 'error') {
        yield { type: 'error', message: evt.message };
        return;
      }
    }

    if (!lastStructured) {
      yield { type: 'error', message: '生成结束但未拿到最终对象' };
      return;
    }

    const summaries = await this.writeSummaries(
      pctx.plan,
      lastStructured,
      lastInvocationId,
      lastSource,
      options.dryRun,
    );

    yield {
      type: 'done',
      agentInvocationId: lastInvocationId,
      source: lastSource,
      costYuan: lastCost,
      promptTokens: lastPromptTokens,
      completionTokens: lastCompletionTokens,
      structured: lastStructured,
      summaries,
      fallbackReason: lastFallbackReason,
    };
  }

  // ─────────────────────────────────────────────
  // 写库:3 type → 3 行 upsert(共享 agentInvocationId)
  // ─────────────────────────────────────────────

  private async writeSummaries(
    plan: PlanRow,
    output: DraftPlanSummaryOutput,
    agentInvocationId: string,
    source: 'agent' | 'template_fallback',
    dryRun?: boolean,
  ) {
    const items: Array<{ type: SummaryType; planSummaryId: string | null; content: string }> = [
      { type: 'one_page', planSummaryId: null, content: output.onePage },
      { type: 'medical_record', planSummaryId: null, content: output.medicalRecord },
      { type: 'treatment_chain', planSummaryId: null, content: output.treatmentChain },
    ];

    if (dryRun) return items;

    for (const item of items) {
      const row = await this.prisma.planSummary.upsert({
        where: { planId_type: { planId: plan.id, type: item.type } },
        create: {
          hostId: plan.hostId,
          tenantId: plan.tenantId,
          planId: plan.id,
          type: item.type,
          content: item.content,
          status: 'ready',
          source,
          agentInvocationId,
        },
        update: {
          content: item.content,
          status: 'ready',
          source,
          agentInvocationId,
        },
        select: { id: true },
      });
      item.planSummaryId = row.id;
    }
    return items;
  }

  // ─────────────────────────────────────────────
  // input 装配
  // ─────────────────────────────────────────────

  private async loadPlanContext(planId: string): Promise<PlanContext> {
    const plan = await this.prisma.followupPlan.findUnique({
      where: { id: planId },
      include: { reasons: { orderBy: { priorityScore: 'desc' } } },
    });
    if (!plan) throw new NotFoundException(`Plan ${planId} not found`);

    const patient = await this.prisma.patient.findUnique({
      where: { id: plan.patientId },
    });
    if (!patient) throw new NotFoundException(`Patient ${plan.patientId} not found`);

    const persona = await this.prisma.persona.findFirst({
      where: { patientId: patient.id, supersededAt: null },
      include: { features: true },
      orderBy: { version: 'desc' },
    });

    const facts = await this.prisma.patientFact.findMany({
      where: {
        hostId: plan.hostId,
        tenantId: plan.tenantId,
        patientId: patient.id,
        status: 'active',
      },
      orderBy: { occurredAt: 'desc' },
    });

    return { plan, patient, persona, facts };
  }

  private buildCallInput(ctx: PlanContext): DraftPlanSummaryInput {
    const { plan, patient, persona, facts } = ctx;
    // v2.1:chain-composer 读独立 diagnosis/treatment/recommendation fact,传所有 facts
    const chains = this.chainComposer.compose(
      facts.map((f) => ({
        id: f.id,
        type: f.type,
        kind: f.kind,
        status: f.status,
        occurredAt: f.occurredAt,
        plannedFor: (f as { plannedFor?: Date | null }).plannedFor ?? null,
        content: f.content,
      })),
    );

    return {
      patient: {
        nameMasked: maskName(patient.name),
        gender: patient.gender,
        age: patient.birthDate ? calcAge(patient.birthDate) : null,
      },
      clinicName: plan.targetClinicId ?? '本诊所',
      plan: {
        primaryScenarioLabel: plan.reasons[0]
          ? planScenarioLabel(plan.reasons[0].scenario)
          : '常规复诊',
        priorityScore: plan.priorityScore,
        reasons: plan.reasons.map((r) => ({
          scenarioLabel: planScenarioLabel(r.scenario),
          reason: r.reason,
          priorityScore: r.priorityScore,
        })),
      },
      personaFeatures: (persona?.features ?? []).map((f) => ({
        label: personaFeatureMeta(f.key).label,
        description: f.description,
        score: f.score,
      })),
      facts: facts.map((f) => ({
        type: f.type,
        occurredAt: f.occurredAt?.toISOString() ?? null,
        title: f.title,
        summary: f.summary,
        content: f.content as Record<string, unknown> | null,
      })),
      chains: chains.map((c) => ({
        name: c.name,
        category: c.category,
        status: c.status,
        currentStage: c.currentStage,
        diagnosedAt: c.diagnosedAt,
        gapDays: c.gapDays,
        nodes: c.nodes.map((n) => ({
          stage: n.stage,
          label: n.label,
          at: n.at,
          done: n.done,
          current: n.current,
        })),
      })),
    };
  }
}

// ─────────────────────────────────────────────
// 内部类型(本地投影,避免循环依赖)
// ─────────────────────────────────────────────

type PlanRow = Prisma.FollowupPlanGetPayload<{ include: { reasons: true } }>;
type PatientRow = Prisma.PatientGetPayload<{}>;
type PersonaWithFeatures = Prisma.PersonaGetPayload<{ include: { features: true } }>;
type FactRow = Prisma.PatientFactGetPayload<{}>;

interface PlanContext {
  plan: PlanRow;
  patient: PatientRow;
  persona: PersonaWithFeatures | null;
  facts: FactRow[];
}
