/**
 * PlanDetailData — 后端 /pac/v1/plans/:id/full 的响应 shape。
 *
 * 由 DemoService.assemble() 产出(命名是历史,服务现在挂在 /plans/:id/full)。
 * 前端 (app)/plans/[planId] 用本类型 + adaptData 转换给 <PlanDetailApp> 渲染。
 */
export type PlanDetailData = {
  /// 该患者「当前活跃」plan id。请求的 plan 已 superseded 时,后端解析为最新版本 id;
  /// 否则 = 请求的 plan id。前端据此 router.replace,避免老 plan URL 停在陈旧快照。
  currentPlanId: string;
  patient: {
    id: string;
    externalId: string;
    medicalRecordNumber: string | null;
    dedicatedCs: { id: string; name: string } | null;
    name: string | null;
    nameMasked: string | null;
    gender: string | null;
    birthDate: string | null;
    age: number | null;
    phone: string | null;
    phoneMasked: string | null;
    active: boolean;
    tags: string[];
  };
  profile: {
    doNotContact: boolean;
    deceased: boolean;
    notes: string | null;
    ltvCents: number;
    paymentCount: number;
    rechargeCount: number;
    refundCount: number;
    firstVisit: string | null;
    lastVisit: string | null;
    daysSinceLatestVisit: number | null;
    doctors: string[];
    contacts: Array<{
      relationship: string;
      relationshipLabel: string;
      name: string | null;
      phone: string | null;
      linked: boolean;
    }>;
    /// ⚠️ TEST ONLY — 监护人(儿童/老人触达 fallback)。真实手机号到位前仅演示用。
    guardian: {
      relationship: string;
      relationshipLabel: string;
      name: string | null;
      phone: string | null;
    } | null;
  } | null;
  plan: {
    id: string;
    version: number;
    status: string;
    priorityScore: number;
    contactAttempts: number;
    maxContactAttempts: number;
    targetClinicId: string | null;
    /// v2.1 召回业务目标(自然语言)— UI "本次目标" 用此渲染
    goal: string | null;
    recommendedAt: string | null;
    recommendedRole: string | null;
    recommendedChannel: string | null;
    assigneeUserId: string | null;
    assignedAt: string | null;
    recycleAt: string | null;
    /// 召回冷静期 / 终态抑制窗到期时间(execution 回写按 outcome 算)。
    /// active+未到期 → 不进召回池(到点浮现);UI 头部"下次回访 / 已暂缓至 X"角标
    snoozedUntil: string | null;
    /// 召回反馈(plan 级)— 标题栏拇指当前态;'up' | 'down' | null
    recallFeedback: 'up' | 'down' | null;
    recallFeedbackNote: string | null;
    /// 该 plan 版本重算时间 — UI "更新于 X" 渲染数据新鲜度
    updatedAt: string;
    reasons: Array<{
      id: string;
      scenario: string;
      subKey: string | null;
      /// 结构化召回信号(DB 存 raw enum,前端用 @pac/types 字典翻译富文本)
      signals: {
        subKey: string;
        triggers: Array<{ type: string; code?: string | null }>;
        toothPosition?: string | null;
        daysSince: number;
        expectedCategories: string[];
      } | null;
      priorityScore: number;
      reason: string;
      lifecycle: string;
      source: string;
      /// W3 末:evidence 改 JSON 统一(跟 PersonaFeature 对齐),前端读 evidence.factIds
      evidence: {
        factIds: string[];
        agentInvocationIds?: string[];
        ruleIds?: string[];
      } | null;
      /// v2.1 算法可解释性 — 6 因子算分 breakdown(详情页"为什么 X 分"渲染数据)
      /// null = scenario plugin 未产 breakdown(老 scenario / 手动添加)
      breakdown:
        | {
            priority: {
              clinicalBase: number;
              timeWindowFactor: number;
              main: number;
              valueBonus: number;
              likelihoodBonus: number;
              urgencyBonus: number;
              confidenceFactor: number;
              raw: number;
            };
            subKey: string | null;
            /// 同 scenario 多 sub-rule 命中合并时,记录所有合并的 subKey
            mergedSubKeys?: string[];
          }
        | null;
    }>;
  } | null;
  persona: {
    id: string;
    version: number;
    computedAt: string;
    features: Array<{
      id: string;
      key: string;
      description: string;
      score: number | null;
      data: unknown;
    }>;
  } | null;
  chains: Array<{
    id: string;
    name: string;
    category: string;
    /// 5 阶段链状态(W3 末从 3 态升级,跟后端 chain-composer 同源)
    status: 'discovered' | 'entered' | 'ongoing' | 'closed';
    currentStage: number;
    target?: boolean;
    estimatedValueCents?: number;
    diagnosedAt?: string;
    gapDays?: number;
    /// 最近一次诊断(re-trigger 证据)+ 该次医生真实医嘱
    latestDxAt?: string;
    latestDxName?: string;
    latestDxAdvice?: string;
    /// 生命周期提示("终身维护(永不闭环)" 等)
    lifecycleNoteZh?: string;
    /// 诊断码(K00–K09)— WhyCard 替代闭环抑制按 code+牙位对齐 reason↔chain
    code?: string;
    /// 桶牙位(cross-chain alternative-closed 判定用,UI 不直接展示)
    toothPosition?: string;
    /// "替代治疗已覆盖"原因 chain.name(K04 后续 K08+种植覆盖等)
    alternativeClosedBy?: string;
    nodes: Array<{
      stage: number;
      title?: string;
      detail?: string;
      doctor?: string;
      at: string;
      hint?: string;
      done?: boolean;
      current?: boolean;
      missing?: boolean;
      /// 旧兼容字段(逐步淘汰)
      label?: string;
      note?: string;
    }>;
  }>;
  facts: Array<{
    id: string;
    type: string;
    kind: string;
    status: string;
    occurredAt: string | null;
    plannedFor: string | null;
    clinicId: string | null;
    title: string | null;
    summary: string | null;
    content: unknown;
  }>;
  /// W4:LLM 生成完会 upsert 到 DB(plan_scripts),此处反 parse 4 段 sections;
  /// null = 该 plan 还未生成话术,前端走 mockScript 兜底 + 引导客服点"重新生成"
  script: {
    id: string;
    status: string;                                      // ready / pending / failed
    source: 'agent' | 'template_fallback' | 'manual';
    generatedAt: string;
    sections: Array<{
      id: string;
      label: string;
      durationHint: string;
      markdown: string;
    }>;
  } | null;
  /// 召回历史(患者级,跨所有 plan 版本,最近 8 条)。
  /// 用途:plan 再次被召回时,客服看到"上次召回结果 / 为什么被暂缓"(终态 outcome 即原因)。
  recallHistory?: Array<{
    id: string;
    outcome: string;          // raw enum,前端用 EXECUTION_OUTCOME_META 翻译 label/tone
    channel: string;
    notes: string | null;
    scheduledNextAt: string | null;
    createdAt: string;
    planVersion: number | null;
  }>;
  /// 诊所回访记录(5 试点,展示用)
  returnVisits?: Array<{
    taskDate: string | null;
    type: string | null;
    status: string | null;
    taskStatus: string | null;
    treatmentItems: string | null;
    followContent: string | null;
    result: string | null;
  }>;
};
