import { Injectable, NotFoundException } from '@nestjs/common';
import type { Prisma } from '@prisma/client';
import { maskName, maskPhone } from '@pac/utils';
import { PrismaService } from '../../prisma/prisma.service';
import { ChainComposerService } from '../plan/engine/chain-composer.service';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';
import { toothSet } from '../sync/pipeline/parsers/tooth-position.util';

/**
 * PlanAggregateService — Plan 详情聚合服务。
 *
 * 一次拉全:patient + profile + plan + reasons + persona + features + facts + chains。
 *
 * 注:命名是历史(早期只给 demo 页用),现在也是 (app)/plans/[planId] 的数据源。
 * 治疗链合成委托给 ChainComposerService(共享逻辑,PlanEngine / PersonaService 同源)。
 */
@Injectable()
export class PlanAggregateService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly chainComposer: ChainComposerService,
  ) {}

  /**
   * 按 planId 查 → 反查 patient → 同 assemble。
   * (app)/plans/[planId] 路由 + PlansAggregateController 的入口。
   */
  async getPlanDetailByPlanId(scope: TenantScopeContext, planId: string) {
    const plan = await this.prisma.followupPlan.findUnique({
      where: { id: planId },
      include: { reasons: { orderBy: { priorityScore: 'desc' } } },
    });
    if (!plan) throw new NotFoundException(`Plan ${planId} not found`);
    if (plan.hostId !== scope.hostId || plan.tenantId !== scope.tenantId) {
      throw new NotFoundException(`Plan ${planId} not found`);
    }
    const patient = await this.prisma.patient.findUnique({
      where: { id: plan.patientId },
      include: { profile: true },
    });
    if (!patient) throw new NotFoundException(`Patient ${plan.patientId} not found`);
    return this.assemble(scope, patient, plan);
  }

  // ─────────────────────────────────────────────
  // 共享 assemble(patient + plan 已加载,组装其他)
  // ─────────────────────────────────────────────

  private async assemble(
    scope: TenantScopeContext,
    patient: Prisma.PatientGetPayload<{ include: { profile: true } }>,
    plan: Prisma.FollowupPlanGetPayload<{ include: { reasons: true } }> | null,
  ) {
    const persona = await this.loadCurrentPersona(patient.id);
    const facts = await this.loadActiveFacts(scope, patient.id);
    // W4:话术从 DB 加载(LLM 流式生成完会 upsert 到 plan_scripts)
    // 没生成过 → script=null,前端走 mock 兜底
    const script = plan ? await this.loadPlanScript(plan.id) : null;
    // v2.1:chain-composer 读独立 diagnosis_record / treatment_record / recommendation_record,
    //       传所有 facts,内部按 type 分组(encounter_record 已只元数据)
    const chains = this.chainComposer.compose(facts);
    // ⭐ SQL 为准:chain ★ 按 plan_reasons (code, tooth) 联合对齐(W4 末)
    //
    // 规则:reason 是 (code + tooth) tuple → 找 chain.code 相同 + tooth overlap
    //   林兆星案例:
    //     K08 reason tooth=17(SQL 召回的是这颗未做)
    //     chain 有 4 条 K08:11;12;21;22 / 15;16;24 / 17 / 未标注
    //     正确 target:只有 chain 17 应 ★(其他都已做 actual implant,SQL ⑤a 排除)
    //   K05 reason tooth=null(全口诊断)→ 任何 K05 chain 都对齐(只 1 条牙周·全口)
    //
    // 1 年前 fulfilled 预约让 chain 算 entered 也照样 ★(SQL 是真理 — 没坚持治疗就该召回)
    const reasonKeys = new Set<string>(); // key = "code|tooth" or "code|*"(全口)
    for (const r of plan?.reasons ?? []) {
      const sigs = (r.signals ?? {}) as { triggers?: Array<{ code?: string }>; toothPosition?: string | null };
      const tooth = (sigs.toothPosition ?? '').trim();
      for (const t of sigs.triggers ?? []) {
        if (!t.code) continue;
        reasonKeys.add(`${t.code}|${tooth || '*'}`);
      }
    }
    // W4 末:牙位精确匹配(Palmer 乳牙 + FDI 恒牙都支持)
    //   旧版 strip 所有字母 → "1D OD" → "1" → Palmer 乳牙 1A/1B/1C/1D/1E 全部误匹配
    //   新版用 toothSet:剥面后缀后保留牙位 base("1D OD" → "1D"; "11 M" → "11")
    //   精确匹配避免 Palmer 5 颗乳牙在儿童 case 被误判
    const toothOverlap = (a: string, b: string): boolean => {
      if (!a || !b) return false;
      const A = toothSet(a);
      if (A.size === 0) return false;
      for (const t of toothSet(b)) {
        if (A.has(t)) return true;
      }
      return false;
    };
    for (const c of chains) {
      if (!c.code) { c.target = false; continue; }
      // closed chain 永不 ★(临床已结束;即使 SQL 召回也是老 plan/data race 异常)
      // K04 46 拔了/被替代关闭 → 即使老 plan reason 还含 K04 也不该 focused
      if (c.status === 'closed') { c.target = false; continue; }
      let hit = false;
      for (const key of reasonKeys) {
        const [code, tooth] = key.split('|');
        if (code !== c.code) continue;
        // reason 无牙位(全口诊断 K05)→ 任何 chain.code 同就命中
        if (tooth === '*') { hit = true; break; }
        // reason 有牙位 → 跟 chain.toothPosition overlap
        if (c.toothPosition && toothOverlap(tooth!, c.toothPosition)) { hit = true; break; }
      }
      c.target = hit;
    }

    return {
      patient: serializePatient(patient),
      profile: serializeProfile(patient, facts, facts.filter((f) => f.type === 'encounter_record')),
      plan: plan ? serializePlan(plan) : null,
      persona: persona ? serializePersona(persona) : null,
      chains,
      facts: facts.map(serializeFact),
      script: script ? serializeScript(script) : null,
    };
  }

  /**
   * W4:加载该 plan 的最新 ready 话术(LLM 生成完会 upsert 进 plan_scripts)。
   * pending/failed 的不返回 — 前端走 mock 兜底,客服点"重新生成"再触发 LLM。
   */
  private loadPlanScript(planId: string) {
    return this.prisma.planScript.findUnique({
      where: { planId },
    });
  }

  // ─────────────────────────────────────────────
  // 数据加载(分小函数 — 便于测试 + 单一职责)
  // ─────────────────────────────────────────────

  private loadCurrentPersona(patientId: string) {
    return this.prisma.persona.findFirst({
      where: { patientId, supersededAt: null },
      include: { features: { orderBy: { createdAt: 'asc' } } },
      orderBy: { version: 'desc' },
    });
  }

  private loadActiveFacts(scope: TenantScopeContext, patientId: string) {
    return this.prisma.patientFact.findMany({
      where: {
        hostId: scope.hostId,
        tenantId: scope.tenantId,
        patientId,
        // ⚠️ 不能只 active — treatment_record(actual) 完成后 status='fulfilled',
        // appointment_record 到诊后也 fulfilled。这些都属"现行已发生事实"应展示。
        // 排除的是被 superseded / cancelled / expired / invalidated 的历史/失效版本。
        status: { in: ['active', 'fulfilled'] },
      },
      orderBy: { occurredAt: 'desc' },
    });
  }
}

// ─────────────────────────────────────────────
// Serializers(纯函数 — 易测、无副作用)
// ─────────────────────────────────────────────

type PatientWithProfile = NonNullable<
  Awaited<ReturnType<PlanAggregateService['getPlanDetailByPlanId']>>['plan']
> extends never
  ? never
  : Parameters<typeof serializePatient>[0];

type Fact = Awaited<ReturnType<typeof loadFactStub>>[number];
// 类型 helper — 仅供推导用,不真调用
declare function loadFactStub(): Promise<
  Awaited<ReturnType<PlanAggregateService['getPlanDetailByPlanId']>>['facts']
>;

function serializePatient(patient: {
  id: string;
  externalId: string;
  name: string | null;
  gender: string | null;
  birthDate: Date | null;
  phone: string | null;
  active: boolean;
  profile: { tags: string[] } | null;
}) {
  return {
    id: patient.id,
    externalId: patient.externalId,
    name: patient.name,
    nameMasked: maskName(patient.name),
    gender: patient.gender,
    birthDate: patient.birthDate?.toISOString().slice(0, 10) ?? null,
    age: patient.birthDate ? calcAge(patient.birthDate) : null,
    phone: patient.phone,
    phoneMasked: maskPhone(patient.phone),
    active: patient.active,
    tags: patient.profile?.tags ?? [],
  };
}

function serializeProfile(
  patient: {
    profile: {
      doNotContact: boolean;
      deceased: boolean;
      primaryContactType: string | null;
      notes: string | null;
    } | null;
  },
  allFacts: Array<{ type: string; content: Prisma.JsonValue; occurredAt: Date | null }>,
  encounters: Array<{ occurredAt: Date | null; content: Prisma.JsonValue }>,
) {
  if (!patient.profile) return null;

  const payments = allFacts.filter((f) => f.type === 'payment_record');
  const recharges = allFacts.filter((f) => f.type === 'recharge_record');
  const refunds = allFacts.filter((f) => f.type === 'refund_record');
  const ltvCents = sumAmount(payments) + sumAmount(recharges) - sumAmount(refunds);

  const latestEncounter = encounters[0];
  const firstEncounter = encounters.at(-1);
  const daysSinceLatestVisit = latestEncounter?.occurredAt
    ? Math.floor((Date.now() - latestEncounter.occurredAt.getTime()) / 86400_000)
    : null;

  const doctors = Array.from(
    new Set(
      encounters
        .map((e) => (e.content as Record<string, unknown> | null)?.doctor_id as string | undefined)
        .filter(Boolean) as string[],
    ),
  ).slice(0, 2);

  return {
    doNotContact: patient.profile.doNotContact,
    deceased: patient.profile.deceased,
    primaryContactType: patient.profile.primaryContactType,
    notes: patient.profile.notes,
    ltvCents,
    paymentCount: payments.length,
    rechargeCount: recharges.length,
    refundCount: refunds.length,
    firstVisit: firstEncounter?.occurredAt?.toISOString().slice(0, 10) ?? null,
    lastVisit: latestEncounter?.occurredAt?.toISOString().slice(0, 10) ?? null,
    daysSinceLatestVisit,
    doctors,
  };
}

function serializePlan(plan: {
  id: string;
  version: number;
  status: string;
  priorityScore: number;
  contactAttempts: number;
  targetClinicId: string | null;
  goal: string | null;
  recommendedAt: Date | null;
  recommendedRole: string | null;
  recommendedChannel: string | null;
  assigneeUserId: string | null;
  assignedAt: Date | null;
  recycleAt: Date | null;
  reasons: Array<{
    id: string;
    scenario: string;
    subKey: string | null;
    priorityScore: number;
    reason: string;
    lifecycle: string;
    source: string;
    evidence: Prisma.JsonValue;
    breakdown: Prisma.JsonValue;
    signals: Prisma.JsonValue;
  }>;
}) {
  return {
    id: plan.id,
    version: plan.version,
    status: plan.status,
    priorityScore: plan.priorityScore,
    contactAttempts: plan.contactAttempts,
    maxContactAttempts: 4, // 应用层默认(后续可挂 host 配置)
    targetClinicId: plan.targetClinicId,
    goal: plan.goal,
    recommendedAt: plan.recommendedAt?.toISOString() ?? null,
    recommendedRole: plan.recommendedRole,
    recommendedChannel: plan.recommendedChannel,
    assigneeUserId: plan.assigneeUserId,
    assignedAt: plan.assignedAt?.toISOString() ?? null,
    recycleAt: plan.recycleAt?.toISOString() ?? null,
    reasons: plan.reasons.map((r) => ({
      id: r.id,
      scenario: r.scenario,
      subKey: r.subKey,
      priorityScore: r.priorityScore,
      reason: r.reason,
      lifecycle: r.lifecycle,
      source: r.source,
      /// W3 末:evidence 改 JSON 统一(跟 PersonaFeature.evidence 对齐),透 JSON 整体
      evidence: r.evidence,
      /// v2.1:6 因子算法 breakdown + subKey,UI 详情页"为什么 73 分"渲染数据来源
      /// (不含 ruleIds — 算法版本追溯靠 git + plan.createdAt)
      breakdown: r.breakdown,
      /// W3 末:结构化召回信号(raw enum / canonical code,前端字典翻译富文本)
      signals: r.signals,
    })),
  };
}

function serializePersona(persona: {
  id: string;
  version: number;
  computedAt: Date;
  features: Array<{ id: string; key: string; description: string; score: number | null }>;
}) {
  return {
    id: persona.id,
    version: persona.version,
    computedAt: persona.computedAt.toISOString(),
    features: persona.features.map((f) => ({
      id: f.id,
      key: f.key,
      description: f.description,
      score: f.score,
    })),
  };
}

/**
 * W4 新加:PlanScript serializer + markdown → sections 反 parse。
 *
 * 后端写库时 plan-script.orchestrator.renderMarkdown 把 4 段拼成:
 *   > 患者:xxx · 语气:xxx
 *   ## 开场\n{opening md}\n## 切入话题\n{followup md}\n## 异议处理\n{objection md}\n## 结束 · 信息确认\n{close md}
 * 这里反向用 regex 按 H2 标题切回 4 段,前端拿到的 sections shape 跟 mockScript 完全一致。
 *
 * 设计决策:**前端单一消费 sections 接口**(mock / 真实 / 流式三路同 shape),
 * 反 parse 集中在后端做,避免前端多套渲染分支。
 */
function serializeScript(s: {
  id: string;
  content: string | null;
  source: string | null;
  status: string;
  generatedAt?: Date | null;
  updatedAt: Date;
}) {
  const sections = parseScriptMarkdownToSections(s.content ?? '');
  return {
    id: s.id,
    status: s.status,
    source: (s.source as 'agent' | 'template_fallback' | 'manual' | null) ?? 'agent',
    generatedAt: (s.generatedAt ?? s.updatedAt).toISOString(),
    sections,
  };
}

const SECTION_HEAD_TO_ID: Record<string, 'opening' | 'followup' | 'objection' | 'close'> = {
  开场: 'opening',
  切入话题: 'followup',
  异议处理: 'objection',
  '结束 · 信息确认': 'close',
};
const SECTION_META: Record<
  'opening' | 'followup' | 'objection' | 'close',
  { label: string; durationHint: string }
> = {
  opening: { label: '开场', durationHint: '30 秒' },
  followup: { label: '切入话题', durationHint: '1–2 分钟' },
  objection: { label: '异议处理', durationHint: '按需' },
  close: { label: '结束 · 信息确认', durationHint: '30 秒' },
};

function parseScriptMarkdownToSections(md: string) {
  // 按 H2 切分:`^## (xxx)$` 之后到下一个 H2 之间为内容
  const ids: Array<'opening' | 'followup' | 'objection' | 'close'> = [
    'opening',
    'followup',
    'objection',
    'close',
  ];
  const result = ids.map((id) => ({
    id,
    label: SECTION_META[id].label,
    durationHint: SECTION_META[id].durationHint,
    markdown: '',
  }));
  if (!md) return result;

  const lines = md.split('\n');
  let currentId: 'opening' | 'followup' | 'objection' | 'close' | null = null;
  const buf: Record<string, string[]> = {};
  for (const line of lines) {
    const m = /^##\s+(.+?)\s*$/.exec(line);
    if (m) {
      const head = (m[1] ?? '').trim();
      const matched = SECTION_HEAD_TO_ID[head];
      currentId = matched ?? null;
      if (currentId) buf[currentId] = [];
      continue;
    }
    if (currentId) {
      buf[currentId] = buf[currentId] ?? [];
      buf[currentId]!.push(line);
    }
  }
  for (const sec of result) {
    sec.markdown = (buf[sec.id] ?? []).join('\n').trim();
  }
  return result;
}

function serializeFact(f: {
  id: string;
  type: string;
  kind: string;
  status: string;
  occurredAt: Date | null;
  plannedFor: Date | null;
  clinicId: string | null;
  title: string | null;
  summary: string | null;
  content: Prisma.JsonValue;
}) {
  return {
    id: f.id,
    type: f.type,
    kind: f.kind,
    status: f.status,
    occurredAt: f.occurredAt?.toISOString() ?? null,
    // planned 类(预约/计划治疗)时间在 plannedFor;透传给前端时间轴排序兜底
    plannedFor: f.plannedFor?.toISOString() ?? null,
    clinicId: f.clinicId,
    title: f.title,
    summary: f.summary,
    content: f.content,
  };
}

// ─────────────────────────────────────────────
// 小工具
// ─────────────────────────────────────────────
/// payment/recharge/refund.content 实际字段是 `amount_cents`(parser 写库时归一为 cents int);
/// 老代码读 `c.amount` 是字段名错配,导致 LTV 永远 0(实测路遥 ¥58 显示成 0)。W3 末修正。
function sumAmount(facts: Array<{ content: Prisma.JsonValue }>): number {
  let total = 0;
  for (const f of facts) {
    const c = f.content as Record<string, unknown>;
    total += Number(c?.amount_cents ?? c?.amount ?? 0);
  }
  return total;
}

function calcAge(birthDate: Date): number {
  const now = new Date();
  let age = now.getFullYear() - birthDate.getFullYear();
  const m = now.getMonth() - birthDate.getMonth();
  if (m < 0 || (m === 0 && now.getDate() < birthDate.getDate())) age--;
  return age;
}
