import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { maskName } from '@pac/utils';
import {
  PERSONA_FEATURE_SPECS,
  subLabelZh,
  diagnosisCodeNameZh,
  treatmentCategoryNameZh,
} from '@pac/types';
import { PrismaService } from '../../../prisma/prisma.service';
import { AiCallRunnerService } from '../ai-call-runner.service';
import { DraftRecallBriefCall } from '../calls/draft-recall-brief/call';
import type { DraftRecallBriefInput } from '../calls/draft-recall-brief/input.types';
import type { TenantScopeContext } from '../../../common/decorators/tenant-scope.decorator';

export interface RecallBriefResult {
  /** 一句话召回简报;status='empty' 时为 null */
  summary: string | null;
  status: 'ready' | 'empty';
  source?: 'agent' | 'template_fallback';
}

/** plan_reasons.signals 形态(跟前端 ReasonLine 契约一致)。 */
interface ReasonSignals {
  subKey?: string | null;
  triggers?: Array<{ type?: string; code?: string | null }>;
  toothPosition?: string | null;
  daysSince?: number;
  expectedCategories?: string[];
}

/** 把天数转人读文本(server 端轻量版,无需精确到日)。 */
function daysText(d: number): string {
  if (!Number.isFinite(d) || d <= 0) return '近期';
  if (d < 60) return `${d} 天`;
  const months = Math.round(d / 30);
  if (months < 12) return `${d} 天(约 ${months} 个月)`;
  const years = (d / 365).toFixed(1).replace(/\.0$/, '');
  return `${d} 天(约 ${years} 年)`;
}

/**
 * RecallBriefOrchestrator — 本次召回「一句话简报」get-or-generate。
 *
 *   有 ready 的 plan_summaries[recall_brief] → 直接返回(秒回,不调 LLM)。
 *   没有 → 取 召回原因(plan_reasons) + 历史治疗(treatment_record actual 计数) + 画像(persona_features)
 *          → 跑 AiCall(回答 谁/解决什么/到诊做什么 三问)→ upsert 存库 → 返回。
 *   无召回原因 → status='empty'。
 *
 * 与 Recall/PersonaSummaryOrchestrator 平行,只是输入更全(三类信号合一)。
 */
@Injectable()
export class RecallBriefOrchestrator {
  private readonly logger = new Logger(RecallBriefOrchestrator.name);
  private static readonly TYPE = 'recall_brief' as const;

  constructor(
    private readonly prisma: PrismaService,
    private readonly runner: AiCallRunnerService,
    private readonly call: DraftRecallBriefCall,
  ) {}

  async getOrGenerate(scope: TenantScopeContext, planId: string): Promise<RecallBriefResult> {
    const plan = await this.prisma.followupPlan.findUnique({
      where: { id: planId },
      select: {
        id: true,
        hostId: true,
        tenantId: true,
        patientId: true,
        reasons: {
          orderBy: { priorityScore: 'desc' },
          select: { scenario: true, subKey: true, reason: true, signals: true },
        },
      },
    });
    if (!plan || plan.hostId !== scope.hostId || plan.tenantId !== scope.tenantId) {
      throw new NotFoundException(`Plan ${planId} not found`);
    }
    if (plan.reasons.length === 0) return { summary: null, status: 'empty' };

    // 1) 已生成 → 直接返回
    const existing = await this.prisma.planSummary.findUnique({
      where: { planId_type: { planId: plan.id, type: RecallBriefOrchestrator.TYPE } },
      select: { content: true, status: true, source: true },
    });
    if (existing?.status === 'ready' && existing.content) {
      return {
        summary: existing.content,
        status: 'ready',
        source: (existing.source as 'agent' | 'template_fallback' | null) ?? undefined,
      };
    }

    // 2) 召回原因(翻译成中文)
    const reasons = plan.reasons.map((r) => {
      const s = (r.signals ?? {}) as ReasonSignals;
      const code = (s.triggers ?? []).find((t) => /^K\d/i.test(t.code ?? ''))?.code ?? null;
      // 触发感知(同 ReasonLine/话术):lead=最高优先来源(诊断>建议>影像)。
      //   lead=建议(无诊断)→ subLabel 用建议名("建议种植"),不套子场景诊断词"缺失牙未启动修复"。
      const lead = (s.triggers ?? [])[0];
      const isRecLead = lead?.type === 'recommendation' && !!lead.code;
      const source: 'diagnosis' | 'recommendation' | 'image_finding' =
        lead?.type === 'recommendation'
          ? 'recommendation'
          : lead?.type === 'image_finding'
            ? 'image_finding'
            : 'diagnosis';
      return {
        subLabel: isRecLead
          ? diagnosisCodeNameZh(lead.code as string)
          : subLabelZh(r.scenario, s.subKey ?? '') || r.scenario,
        source,
        diagnosis: code ? diagnosisCodeNameZh(code) : null,
        tooth: (s.toothPosition ?? '').trim() || null,
        daysSinceText: daysText(s.daysSince ?? 0),
        expectedCategories: (s.expectedCategories ?? []).map((c) => treatmentCategoryNameZh(c)),
      };
    });

    // 3) 历史治疗:treatment_record actual 按 category 计数
    const txFacts = await this.prisma.patientFact.findMany({
      where: {
        hostId: scope.hostId,
        tenantId: scope.tenantId,
        patientId: plan.patientId,
        type: 'treatment_record',
        kind: 'actual',
      },
      select: { content: true, occurredAt: true },
    });
    const catCount = new Map<string, number>();
    // 复查切入锚点(第④问事实依据):最近一次实际治疗 + 最近一次洁牙/检查(preventive/periodontic)
    let lastVisitAt: Date | null = null;
    let lastCleaningAt: Date | null = null;
    const CLEANING_CATEGORIES = new Set(['preventive', 'periodontic']);
    for (const f of txFacts) {
      const cat = String((f.content as Record<string, unknown> | null)?.category ?? '').trim();
      if (!cat) continue;
      catCount.set(cat, (catCount.get(cat) ?? 0) + 1);
      if (f.occurredAt) {
        if (!lastVisitAt || f.occurredAt > lastVisitAt) lastVisitAt = f.occurredAt;
        if (CLEANING_CATEGORIES.has(cat) && (!lastCleaningAt || f.occurredAt > lastCleaningAt)) {
          lastCleaningAt = f.occurredAt;
        }
      }
    }
    const now = Date.now();
    const anchorText = (d: Date | null): string | null => {
      if (!d) return null;
      const days = Math.floor((now - d.getTime()) / 86_400_000);
      return days <= 0 ? '近期' : `${daysText(days)}前`;
    };
    const reviewAnchors = {
      lastVisitText: anchorText(lastVisitAt),
      lastCleaningText: anchorText(lastCleaningAt),
    };

    // 3.5) 医生计划/建议/医嘱(第④问切入的最优先事实依据,统一带 verbatim 原话):
    //   治疗计划 = treatment_record kind='planned' / 医生建议 = recommendation_record /
    //   医嘱 = emr_record.content.doctor_advice(真实医嘱文本;"常嘱"级短水词丢弃)
    const clip = (s: string, max: number): string => (s.length > max ? `${s.slice(0, max)}…` : s);
    const [plannedFacts, recFacts, emrFacts] = await Promise.all([
      this.prisma.patientFact.findMany({
        where: {
          hostId: scope.hostId,
          tenantId: scope.tenantId,
          patientId: plan.patientId,
          type: 'treatment_record',
          kind: 'planned',
        },
        orderBy: { occurredAt: 'desc' },
        take: 3,
        select: { content: true, occurredAt: true },
      }),
      this.prisma.patientFact.findMany({
        where: {
          hostId: scope.hostId,
          tenantId: scope.tenantId,
          patientId: plan.patientId,
          type: 'recommendation_record',
        },
        orderBy: { occurredAt: 'desc' },
        take: 3,
        select: { content: true, occurredAt: true },
      }),
      this.prisma.patientFact.findMany({
        where: {
          hostId: scope.hostId,
          tenantId: scope.tenantId,
          patientId: plan.patientId,
          type: 'emr_record',
        },
        orderBy: { occurredAt: 'desc' },
        take: 5,
        select: { content: true, occurredAt: true },
      }),
    ]);
    const doctorGuidance: DraftRecallBriefInput['doctorGuidance'] = [
      ...plannedFacts.map((f) => {
        const c = (f.content ?? {}) as Record<string, unknown>;
        const raw = String(c.subtype ?? '').trim();
        return {
          kind: 'plan' as const,
          label: treatmentCategoryNameZh(String(c.category ?? '')),
          verbatim: raw ? clip(raw, 40) : null,
          tooth: String(c.tooth_position ?? '').trim() || null,
          atText: anchorText(f.occurredAt),
        };
      }),
      ...recFacts.map((f) => {
        const c = (f.content ?? {}) as Record<string, unknown>;
        const code = String(c.code ?? '').trim();
        const name = String(c.name ?? '').trim();
        return {
          kind: 'recommendation' as const,
          label: code ? diagnosisCodeNameZh(code) : '医生建议',
          verbatim: name ? clip(name, 40) : null,
          tooth: String(c.tooth_position ?? '').trim() || null,
          atText: anchorText(f.occurredAt),
        };
      }),
      // 医嘱:只收有信息量的(≥6 字过滤"常嘱"级水词),最近 2 条
      ...emrFacts
        .map((f) => ({
          advice: String(((f.content ?? {}) as Record<string, unknown>).doctor_advice ?? '').trim(),
          occurredAt: f.occurredAt,
        }))
        .filter((x) => x.advice.length >= 6)
        .slice(0, 2)
        .map((x) => ({
          kind: 'advice' as const,
          label: '医嘱',
          verbatim: clip(x.advice, 40),
          tooth: null,
          atText: anchorText(x.occurredAt),
        })),
    ];
    const treatmentHistory = [...catCount.entries()]
      .sort((a, b) => b[1] - a[1])
      .map(([cat, count]) => ({ category: treatmentCategoryNameZh(cat), count }));

    // 4) 画像标签
    const persona = await this.prisma.persona.findFirst({
      where: { patientId: plan.patientId, supersededAt: null },
      orderBy: { version: 'desc' },
      select: { features: { orderBy: { createdAt: 'asc' }, select: { key: true, description: true } } },
    });
    const personaItems = (persona?.features ?? []).map((f) => ({
      label: PERSONA_FEATURE_SPECS[f.key]?.nameZh ?? f.key,
      value: f.description,
    }));

    const patient = await this.prisma.patient.findUnique({
      where: { id: plan.patientId },
      select: { name: true },
    });

    const input: DraftRecallBriefInput = {
      patientNameMasked: maskName(patient?.name ?? null) ?? '该患者',
      persona: personaItems,
      treatmentHistory,
      reasons,
      reviewAnchors,
      doctorGuidance,
    };

    // 5) 跑 AiCall → upsert
    const result = await this.runner.run(this.call, input, {
      hostId: plan.hostId,
      tenantId: plan.tenantId,
      workflowRunId: randomUUID(),
      linkedPatientId: plan.patientId,
      linkedPlanId: plan.id,
      bustCache: false,
      evalMode: 'production',
    });

    await this.prisma.planSummary.upsert({
      where: { planId_type: { planId: plan.id, type: RecallBriefOrchestrator.TYPE } },
      create: {
        hostId: plan.hostId,
        tenantId: plan.tenantId,
        planId: plan.id,
        type: RecallBriefOrchestrator.TYPE,
        content: result.output.summary,
        status: 'ready',
        source: result.source,
        agentInvocationId: result.invocationId,
      },
      update: {
        content: result.output.summary,
        status: 'ready',
        source: result.source,
        agentInvocationId: result.invocationId,
      },
    });

    return { summary: result.output.summary, status: 'ready', source: result.source };
  }
}
