/**
 * 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;
    /// 品牌 GUID(source_unit 经 host.orgAliases 反解)—— EMR 跳转链接 {brandId} 用;无别名则 null
    brandId: 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;
    /// 号码真实性:true = 已核实真号;false = 宿主同步号(测试库为造数假号)
    phoneVerified: boolean;
    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;
      /// 是不是亲戚(后端按 KIN_RELATIONSHIPS 判;friend/other 不算)—— 只有亲戚进「关联客户」行
      isKin: boolean;
      age: number | null;
      relatedPatientId: string | null;
      /// 关系人在本 scope 内可打开的工单;null = 没有(或越权)→ 前端不给链接,免得点进去 404
      planId: string | null;
      /// 关系人的宿主侧 id / 病历号 —— 填 VIEW_PATIENT 槽位占位用(配了就跳宿主档案)
      externalId: string;
      medicalRecordNumber: string | null;
      /// 源数据原样的关系码;与 relationship 不同 = 被年龄纠正过
      relationshipRaw: string;
      /// 关系方向被年龄纠正过 —— UI 给一句 hover 说明,让客服知道这不是源数据原文
      ageCorrected: 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[];
        /** 按年龄适配后的建议治疗主类目;旧 plan 无此字段 → 回落 expectedCategories[0] */
        focusCategory?: string | null;
        /** plan 生成时的年龄快照(无生日 → null);展示按此措辞 */
        patientAge?: number | null;
        /** 触发诊断原文词快照(如「乳牙早失」);K00 细分用,旧 plan 无 → 回落原顺序 */
        dxNameZh?: string | null;
      } | null;
      priorityScore: number;
      reason: string;
      lifecycle: string;
      source: string;
      /// W3 末:evidence 改 JSON 统一(跟 PersonaFeature 对齐),前端读 evidence.factIds
      evidence: {
        factIds: string[];
        agentInvocationIds?: string[];
        ruleIds?: string[];
      } | null;
      /// v3.0 算法可解释性 — 三维 breakdown(详情页"为什么 X 分"渲染数据)
      /// null = scenario plugin 未产 breakdown(老 scenario / 手动添加)
      breakdown:
        | {
            priority: {
              urgency: number; // 急迫性 0-10
              value: number; // 价值性 0-10
              willingness: number; // 意愿度 0-10
              rfmAdherence: number; // 意愿·RFM依从
              intentBehavior: number; // 意愿·主诉行为
              trustBase: number; // 意愿·信任基础
              freshness: number; // 新鲜度因子 0.4-1
              confidenceFactor: number; // 置信度因子 0.75-1
              base: number; // 三维加权
              raw: number; // 综合 ×因子后
            };
            subKey: string | null;
            /// 同 scenario 多 sub-rule 命中合并时,记录所有合并的 subKey
            mergedSubKeys?: string[];
          }
        | null;
    }>;
  } | null;
  persona: {
    id: string;
    version: number;
    /// 本版本创建时刻(升版本才动)
    computedAt: string;
    /// 内容真实新鲜度:只有天数漂移时后端就地刷新、不升版本 → computedAt 会停在旧值。
    /// UI 的「更新于」用这个。老响应可能没有 → 调用方回退 computedAt。
    refreshedAt?: string;
    features: Array<{
      id: string;
      key: string;
      description: string;
      score: number | null;
      data: unknown;
      /// 证据 fact id(反查同一响应的 facts 渲染「依据」);老响应没有 → 空
      evidenceFactIds?: string[];
    }>;
  } | 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;
  }>;
};
