// PAC — Patient Analyze Center
// 三层模型:Patient Facts(事实)→ Persona(画像)→ Follow-up Plan(计划),
// 加上 Agent Pool 调用审计、Plan 执行回写。
//
// 多租户隔离:每张业务表都带 platform_id + tenant_id + clinic_id 三级,联合索引,
// 查询层强制按这三个 ID 过滤。
//   platform_id = 哪个宿主接入了我们(friday / 5i5y / dw / mock)
//   tenant_id   = 宿主自己的客户/组织 id
//   clinic_id   = 该租户下的某家诊所

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// =============================================================
// Platform / 平台租户注册
// =============================================================

/// 接入 PAC 的宿主平台注册表。每个 appId 唯一,记录鉴权秘钥哈希、同步配置、字段映射、规则。
///
/// 重要架构观:**PAC 永远同时支持 pull 和 push 两种数据接入模式**,这是 PAC 的能力,
/// 不是配置项。本表里 pullConfig / pushSecretHash 只是表达"这个特定平台是否参与某种模式":
///   - pullConfig 不为 null  → PAC 会按计划去拉这个平台
///   - pushSecretHash 不为 null → PAC 接受用该 HMAC 密钥签名的 push
///   - 两者都为 null → 该平台不贡献数据(但仍可换票消费 API Pool 2)
///   - 两者都填 → 同时支持
model Platform {
  /// 主键 UUID
  id            String @id @default(uuid()) @db.Uuid
  /// 平台标识(friday / 5i5y / dw / demo),业务可读
  platformId    String @unique @map("platform_id")
  /// 平台中文名,用于后台展示
  name          String
  /// 宿主调换票接口用的 appId
  appId         String @unique @map("app_id")
  /// appSecret 的 scrypt 哈希(salt:hash 格式),原文从不落库
  appSecretHash String @map("app_secret_hash")

  /// PAC 拉取该平台所需的配置;null = PAC 不主动拉这个平台。
  /// 形状: { loginUrl?, credentials?{username,password}, apiKey?, endpoints{patients?,...}, intervalMinutes? }
  pullConfig     Json?   @map("pull_config")

  /// 接受该平台 push 时验签用的 HMAC 共享密钥的哈希;null = 拒绝该平台的所有 push。
  pushSecretHash String? @map("push_secret_hash")

  /// 字段映射:左边是 PAC 内部字段名,右边是该宿主返回的字段名。
  /// 形状: { patient: { phone: "mobile", externalId: "pid" }, diagnosis: { ... } }
  /// 这是 platform-level 配置真正必要的部分 — 各宿主字段命名不同无法纯文档约定。
  fieldMapping   Json    @default("{}") @map("field_mapping")

  /// 召回规则配置(漏诊阈值、回收超时、优先级权重等)
  rulesConfig Json     @default("{}") @map("rules_config")
  /// 动作跳转 URL 模板(VIEW_PATIENT、CREATE_APPOINTMENT 等)
  actionUrls  Json     @default("{}") @map("action_urls")
  /// 是否启用,false 时换票直接 401
  active      Boolean  @default(true)
  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")

  @@map("platforms")
}

// =============================================================
// Layer 1: 患者事实
// =============================================================

/// 患者档案。来自宿主系统,本地冗余存储以便业务计算不依赖宿主实时接口。
/// (platform_id, tenant_id, external_id) 唯一,external_id = 宿主系统里的患者 id。
///
/// 召回池查询第一行硬过滤:
///   WHERE status='active' AND do_not_contact=false AND deceased=false
model Patient {
  id          String    @id @default(uuid()) @db.Uuid
  platformId  String    @map("platform_id")
  tenantId    String    @map("tenant_id")
  clinicId    String    @map("clinic_id")
  /// 宿主系统里的患者 id,用来回链
  externalId  String    @map("external_id")
  /// 患者姓名
  name        String
  /// 联系电话
  phone       String
  /// 性别(自由文本,各宿主取值不一致,不强枚举)
  gender      String?
  /// 出生日期
  birthDate   DateTime? @map("birth_date") @db.Date
  /// 偏好(沟通时间、语言、就诊偏好等),自由 JSON
  preferences Json?

  /// 不打扰名单。患者主动 opt-out / 投诉过 / 主动要求停止回访等情况置 true。
  /// 一旦 true 永久从召回池排除(合规硬要求)。
  doNotContact       Boolean   @default(false) @map("do_not_contact")
  /// 不打扰原因(complaint / opt_out / treated_elsewhere / ...),用于分析与回看
  doNotContactReason String?   @map("do_not_contact_reason")
  /// 何时打的标,审计 + 看是否过冷静期
  doNotContactAt     DateTime? @map("do_not_contact_at")

  /// 已故标记。不能给已故患者发回访,医疗 SaaS 必须支持。
  deceased    Boolean   @default(false)
  deceasedAt  DateTime? @map("deceased_at")

  /// 档案有效性。active 才进召回池;archived 表示宿主软删除;merged 表示合并到了 mergedIntoId。
  status       String   @default("active")
  mergedIntoId String?  @map("merged_into_id") @db.Uuid

  createdAt   DateTime  @default(now()) @map("created_at")
  updatedAt   DateTime  @updatedAt @map("updated_at")

  factEvents   PatientFactEvent[]
  diagnoses    Diagnosis[]
  treatments   Treatment[]
  appointments Appointment[]
  charges      Charge[]
  touchpoints  Touchpoint[]
  personas     Persona[]
  plans        FollowupPlan[]

  @@unique([platformId, tenantId, externalId])
  @@index([platformId, tenantId, clinicId])
  @@index([phone])
  /// 召回池热查询路径:租户 + 诊所 + 三个状态硬过滤
  @@index([platformId, tenantId, clinicId, status, doNotContact, deceased])
  @@map("patients")
}

/// 患者事实时间轴(append-only 日志)。所有"发生了什么"的真理来源 —
/// 快照表(Diagnosis / Treatment / Appointment / Charge / Touchpoint)是它的投影。
/// **接口 2 的 PlanExecution 绝不写到这里**(架构 §5.3)。
model PatientFactEvent {
  id         String @id @default(uuid()) @db.Uuid
  platformId String @map("platform_id")
  tenantId   String @map("tenant_id")
  clinicId   String @map("clinic_id")
  patientId  String @map("patient_id") @db.Uuid

  /// 事实类型,canonical 值见 FactType 枚举(允许宿主扩展)
  factType    String  @map("fact_type")
  /// 事件进 PAC 的方向: pull / push / manual
  source      String
  /// 关联到**该事件对应的宿主侧具体记录 id**(治疗 id / 诊断 id / 预约 id / 缴费 id 等);
  /// `patient_registered` 这种患者级事件填 patient 的 external_id。可空(manual / mock 可不填)
  externalRef String? @map("external_ref")

  /// 事实负载(无 schema,按 fact_type 在应用层用 zod 校验)。
  /// **不要在 payload 里重复存 PII**(姓名/电话已在 patients 主档),只存该次事件特有内容。
  payload Json

  /// 事实在宿主侧实际发生时间(由 adapter 从宿主返回的时间字段提取)
  occurredAt DateTime @map("occurred_at")
  /// PAC 接收到事实的时间(同步监控用)
  ingestedAt DateTime @default(now()) @map("ingested_at")

  patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)

  /// 幂等性约束:同 (platform, tenant, factType, externalRef) 只允许一条;
  /// externalRef 为 NULL 时 PostgreSQL 视为不同(允许 manual / mock 重复入)
  @@unique([platformId, tenantId, factType, externalRef])
  @@index([platformId, tenantId, clinicId])
  @@index([patientId, occurredAt])
  @@index([factType])
  @@index([ingestedAt])
  @@map("patient_fact_events")
}

/// 诊断记录快照。从 fact 时间轴投影出来,便于快查。
model Diagnosis {
  id            String   @id @default(uuid()) @db.Uuid
  platformId    String   @map("platform_id")
  tenantId      String   @map("tenant_id")
  clinicId      String   @map("clinic_id")
  /// 宿主系统里的诊断记录 id
  externalId    String   @map("external_id")
  patientId     String   @map("patient_id") @db.Uuid
  /// 主诊医生 id
  doctorId      String?  @map("doctor_id")
  /// 诊断编码(ICD / 宿主自定义)
  diagnosisCode String?  @map("diagnosis_code")
  /// 诊断名称(自由文本)
  diagnosisName String   @map("diagnosis_name")
  /// 诊断时间
  diagnosedAt   DateTime @map("diagnosed_at")
  /// 备注
  notes         String?
  createdAt     DateTime @default(now()) @map("created_at")
  updatedAt     DateTime @updatedAt @map("updated_at")

  patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)

  @@unique([platformId, tenantId, externalId])
  @@index([platformId, tenantId, clinicId])
  @@index([patientId])
  @@index([diagnosedAt])
  @@map("diagnoses")
}

/// 治疗记录快照。Plan 召回引擎扣减"已完成治疗"的依据。
model Treatment {
  id            String    @id @default(uuid()) @db.Uuid
  platformId    String    @map("platform_id")
  tenantId      String    @map("tenant_id")
  clinicId      String    @map("clinic_id")
  /// 宿主系统里的治疗记录 id
  externalId    String    @map("external_id")
  patientId     String    @map("patient_id") @db.Uuid
  /// 关联诊断 id(同库内 UUID)
  diagnosisId   String?   @map("diagnosis_id") @db.Uuid
  /// 治疗项目名(根管治疗 / 种植 / 正畸 ...)
  treatmentName String    @map("treatment_name")
  /// 完成时间(null = 未完成)
  completedAt   DateTime? @map("completed_at")
  /// 治疗状态(进行中 / 已完成 / 已取消等,各宿主自定义)
  status        String
  createdAt     DateTime  @default(now()) @map("created_at")
  updatedAt     DateTime  @updatedAt @map("updated_at")

  patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)

  @@unique([platformId, tenantId, externalId])
  @@index([platformId, tenantId, clinicId])
  @@index([patientId])
  @@map("treatments")
}

/// 预约记录快照。Plan 召回引擎扣减"已有预约"线索 + 接口 2 完成时关联新预约 ref。
model Appointment {
  id          String   @id @default(uuid()) @db.Uuid
  platformId  String   @map("platform_id")
  tenantId    String   @map("tenant_id")
  clinicId    String   @map("clinic_id")
  /// 宿主系统里的预约 id
  externalId  String   @map("external_id")
  patientId   String   @map("patient_id") @db.Uuid
  /// 接诊医生 id
  doctorId    String?  @map("doctor_id")
  /// 预约时间
  scheduledAt DateTime @map("scheduled_at")
  /// 预约状态(已确认 / 已到诊 / 已取消等)
  status      String
  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")

  patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)

  @@unique([platformId, tenantId, externalId])
  @@index([platformId, tenantId, clinicId])
  @@index([patientId])
  @@index([scheduledAt])
  @@map("appointments")
}

/// 缴费记录快照。Persona 的"客户价值"特征算消费总额时直接 SUM 这张表。
/// 退款情况:同 external_id 的 charge 用 status 反映(paid / refunded / partial_refund),
/// 不再写一条新记录;refund_amount_cents 表达已退金额。
model Charge {
  id            String   @id @default(uuid()) @db.Uuid
  platformId    String   @map("platform_id")
  tenantId      String   @map("tenant_id")
  clinicId      String   @map("clinic_id")
  /// 宿主系统里的缴费/订单 id
  externalId    String   @map("external_id")
  patientId     String   @map("patient_id") @db.Uuid
  /// 关联的治疗(若有,可空 — 缴费可能是诊费 / 套餐 / 其他)
  treatmentId   String?  @map("treatment_id") @db.Uuid

  /// 金额(分为单位,避免浮点;1元 = 100 分)
  amountCents       Int     @map("amount_cents")
  /// 已退金额(分)。partial_refund 时小于 amountCents,refunded 时等于
  refundAmountCents Int     @default(0) @map("refund_amount_cents")
  /// 币种,默认 CNY
  currency          String  @default("CNY")

  /// 类别(treatment / consultation / membership / other),由宿主映射
  category String?

  /// 状态:paid / refunded / partial_refund / cancelled
  status String

  /// 缴费时间(宿主侧的 paid_at / charged_at)
  chargedAt   DateTime  @map("charged_at")
  /// 退款时间(可空)
  refundedAt  DateTime? @map("refunded_at")
  createdAt   DateTime  @default(now()) @map("created_at")
  updatedAt   DateTime  @updatedAt @map("updated_at")

  patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)

  @@unique([platformId, tenantId, externalId])
  @@index([platformId, tenantId, clinicId])
  @@index([patientId, chargedAt])
  @@index([status])
  @@map("charges")
}

/// 触点记录快照(Touchpoint)— 宿主已有的"患者接触"事实(架构 §1 数据边界中的"已有回访/跟进事实")。
/// 用途:
///   1. PlanEngine 召回算法排除"近 N 天宿主已经接触过的患者"
///   2. OnePageSummary 显示"该患者上次被接触是 X 时间 / Y 渠道"
///   3. 跨 PAC + 宿主统一最大联系频次熔断
///
/// 跟 PAC 自己的 PlanExecution 区分:Touchpoint 是宿主**已经发生**的接触事实(只读),
/// PlanExecution 是 PAC 主导的回访动作执行回写(写入)。
model Touchpoint {
  id          String   @id @default(uuid()) @db.Uuid
  platformId  String   @map("platform_id")
  tenantId    String   @map("tenant_id")
  clinicId    String   @map("clinic_id")
  /// 宿主侧的触点记录 id
  externalId  String   @map("external_id")
  patientId   String   @map("patient_id") @db.Uuid

  /// 接触发生时间(宿主侧)
  occurredAt  DateTime @map("occurred_at")
  /// 接触渠道(宿主自行定义:phone / wecom / sms / visit / 等;PAC 不强枚举)
  channel     String?
  /// 接触结果(宿主自行定义)
  outcome     String?
  /// 摘要 / 备注
  notes       String?  @db.Text
  /// 宿主侧的执行人 id(回链审计)
  operatorRef String?  @map("operator_ref")

  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")

  patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)

  @@unique([platformId, tenantId, externalId])
  @@index([platformId, tenantId, clinicId])
  @@index([patientId, occurredAt])
  @@map("touchpoints")
}

// =============================================================
// Layer 2: Persona 画像 — 按患者维度版本化
// =============================================================

/// 患者画像(版本流)。每次重算产生新版本,旧版本保留并标 supersededAt。
/// 当前版本 = supersededAt IS NULL 的那条。
model Persona {
  id         String @id @default(uuid()) @db.Uuid
  platformId String @map("platform_id")
  tenantId   String @map("tenant_id")
  clinicId   String @map("clinic_id")
  patientId  String @map("patient_id") @db.Uuid
  /// 该患者的 persona 单调递增版本号
  version    Int

  /// 触发本次重算的来源:"fact:<eventId>" / "manual" / "scheduled"
  triggeredBy  String    @map("triggered_by")
  /// 计算完成时间
  computedAt   DateTime  @default(now()) @map("computed_at")
  /// 被新版本取代的时间;null 表示当前版本
  supersededAt DateTime? @map("superseded_at")

  patient  Patient          @relation(fields: [patientId], references: [id], onDelete: Cascade)
  features PersonaFeature[]
  plans    FollowupPlan[]

  @@unique([patientId, version])
  @@index([platformId, tenantId, clinicId])
  @@index([patientId, supersededAt])
  @@map("personas")
}

/// Persona 的特征条目。
/// 架构强制:每条特征 = 特征键 + 自然语言描述 + 证据引用 + 版本(随父 Persona)。
/// **不要把 description 拆成更细的字段** — 这是设计约束,不是疏漏。
/// numericScore 是排序/筛选的可选数值,不是描述的替代。
model PersonaFeature {
  id        String @id @default(uuid()) @db.Uuid
  personaId String @map("persona_id") @db.Uuid

  /// 特征键(如 "value" / "treatment_chain_status" / "communication_preference")
  featureKey   String  @map("feature_key")
  /// 可选分组(如 "value" / "risk" / "preference")
  category     String?
  /// 自然语言描述,自包含。不要拆字段。
  description  String  @db.Text
  /// 可选数值评分,用于排序/筛选
  numericScore Float?  @map("numeric_score")

  /// 证据引用:{ factEventIds, agentInvocationId, ruleIds } —
  /// 任何 persona 都能反查到"为什么得出这个结论"
  evidence Json

  createdAt DateTime @default(now()) @map("created_at")

  persona Persona @relation(fields: [personaId], references: [id], onDelete: Cascade)

  @@unique([personaId, featureKey])
  @@index([featureKey])
  @@map("persona_features")
}

// =============================================================
// Layer 3: Follow-up Plan 召回计划
// =============================================================

/// 召回计划(版本流)。
/// 召回池 = WHERE status='active' AND assignee_user_id IS NULL 的查询结果。
/// 同一 (patient, scenario) 同时只有一个 ACTIVE/ASSIGNED 版本,新版生成时旧版置为 SUPERSEDED。
model FollowupPlan {
  id         String  @id @default(uuid()) @db.Uuid
  platformId String  @map("platform_id")
  tenantId   String  @map("tenant_id")
  clinicId   String  @map("clinic_id")
  patientId  String  @map("patient_id") @db.Uuid
  /// 生成本计划用的 persona 版本(可空,新患者刚进系统时可能没 persona)
  personaId  String? @map("persona_id") @db.Uuid

  /// 业务场景(post_treatment_recall / 后续扩展...)
  scenario String
  /// 该 (patient, scenario) 维度的版本号
  version  Int

  /// 计划目标(自然语言,如"提醒已完成根管治疗的患者按时复查")
  goal               String?   @db.Text
  /// 优先级分数,召回池排序用
  priorityScore      Float     @map("priority_score")
  /// 该患者为什么进入本次召回 — agent 输出,人类可读
  inclusionReason    String    @map("inclusion_reason") @db.Text
  /// 推荐触达时间
  recommendedAt      DateTime? @map("recommended_at")
  /// 推荐执行人角色(staff / leader / doctor_assistant ...)
  recommendedRole    String?   @map("recommended_role")
  /// 推荐触达渠道(phone / wecom / sms ...)
  recommendedChannel String?   @map("recommended_channel")

  /// 证据引用:{ factEventIds, personaVersion, agentInvocationIds, ruleIds }
  evidence Json

  /// 生命周期状态:active(在池) / assigned / executing / completed / abandoned / recycled / superseded
  status String

  /// 已尝试联系次数(熔断依据)
  contactAttempts Int       @default(0) @map("contact_attempts")
  /// 上次联系时间
  lastContactedAt DateTime? @map("last_contacted_at")
  /// 自动回收时刻(被超时机制扔回池子)
  recycleAt       DateTime? @map("recycle_at")

  /// 当前指派给谁
  assigneeUserId String?   @map("assignee_user_id")
  /// 指派时使用的角色
  assigneeRole   String?   @map("assignee_role")
  /// 指派发生时间
  assignedAt     DateTime? @map("assigned_at")
  /// 指派模式:manual(手动)/ auto(系统分配)
  assignmentMode String?   @map("assignment_mode")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  patient    Patient         @relation(fields: [patientId], references: [id], onDelete: Cascade)
  persona    Persona?        @relation(fields: [personaId], references: [id])
  scripts    PlanScript[]
  summaries  PlanSummary[]
  executions PlanExecution[]

  @@unique([patientId, scenario, version])
  @@index([platformId, tenantId, clinicId])
  @@index([scenario, status])
  @@index([assigneeUserId])
  @@index([priorityScore])
  @@map("followup_plans")
}

/// AI 生成的话术(关联到具体 plan 版本),来源由 agentInvocationId 反查。
model PlanScript {
  id                String   @id @default(uuid()) @db.Uuid
  planId            String   @map("plan_id") @db.Uuid
  /// 话术类型:opening(开场) / follow_up(跟进) / objection_handling(异议处理)
  scriptType        String   @map("script_type")
  /// 话术正文(可能含 Markdown / 段落)
  content           String   @db.Text
  /// 关联到产出本话术的 agent 调用,审计用
  agentInvocationId String?  @map("agent_invocation_id") @db.Uuid
  createdAt         DateTime @default(now()) @map("created_at")

  plan FollowupPlan @relation(fields: [planId], references: [id], onDelete: Cascade)

  @@index([planId])
  @@map("plan_scripts")
}

/// AI 生成的辅助材料(One Page Summary、病历摘要、治疗链概览等),关联 plan 版本。
model PlanSummary {
  id                String   @id @default(uuid()) @db.Uuid
  planId            String   @map("plan_id") @db.Uuid
  /// 材料类型:one_page / medical_record / treatment_chain
  summaryType       String   @map("summary_type")
  /// 材料正文
  content           String   @db.Text
  /// 关联产出 agent 调用,审计用
  agentInvocationId String?  @map("agent_invocation_id") @db.Uuid
  createdAt         DateTime @default(now()) @map("created_at")

  plan FollowupPlan @relation(fields: [planId], references: [id], onDelete: Cascade)

  @@index([planId])
  @@map("plan_summaries")
}

/// 接口 2:Plan 执行回写。客服打完电话或操作后,宿主把结果写到这里。
/// **强约束:这条记录绝不会被提升为 PatientFactEvent**(架构 §5.3),
/// 它只驱动 Plan 自己的生命周期。
model PlanExecution {
  id             String @id @default(uuid()) @db.Uuid
  platformId     String @map("platform_id")
  tenantId       String @map("tenant_id")
  clinicId       String @map("clinic_id")
  planId         String @map("plan_id") @db.Uuid
  /// 操作人 user id(从 access token 取)
  operatorUserId String @map("operator_user_id")

  /// 触达渠道:phone / wecom / sms / other
  channel String

  /// 通话结果:abandoned(放弃)/ no_answer(未接通)/ scheduled_next(下次回访)/ success_appointed(成功 = 新预约)
  outcome String
  /// 本次电话纪要(客服手填的自由文本)
  notes   String? @db.Text

  /// 放弃原因(预设多选,见 AbandonReason 枚举)
  abandonReasons String[] @map("abandon_reasons")
  /// 放弃原因 - "其他" 自由文本
  abandonOther   String?  @map("abandon_other")

  /// 当 outcome=scheduled_next 时,下次回访时间
  scheduledNextAt DateTime? @map("scheduled_next_at")

  /// 当 outcome=success_appointed 时,关联宿主侧新建的预约 id
  newAppointmentRef String? @map("new_appointment_ref")

  /// 通话/操作开始时间
  startedAt DateTime  @default(now()) @map("started_at")
  /// 通话/操作结束时间
  endedAt   DateTime? @map("ended_at")
  createdAt DateTime  @default(now()) @map("created_at")

  plan FollowupPlan @relation(fields: [planId], references: [id], onDelete: Cascade)

  @@index([platformId, tenantId, clinicId])
  @@index([planId])
  @@index([operatorUserId])
  @@map("plan_executions")
}

// =============================================================
// Agent Pool — 每次 AI 调用都落库审计
// =============================================================

/// AI agent 调用记录。每个调用都强制落一条,供合规审计 + 反查"这个输出哪来的"。
/// 对接的 AI 网关 API 形状对齐 Dify(/v1/workflows/run)。
model AgentInvocation {
  id         String @id @default(uuid()) @db.Uuid
  platformId String @map("platform_id")
  tenantId   String @map("tenant_id")
  clinicId   String @map("clinic_id")

  /// agent 类型:script(话术)/ ranking(排序打分)/ plan_analysis(计划解释)/ medical_summary / one_page
  agentKind    String  @map("agent_kind")
  /// agent 版本(prompt 模板版本 / 模型版本组合的标记)
  agentVersion String? @map("agent_version")

  /// 网关后端实际是谁(dify / ai-gateway-passthrough / ...)
  gatewayProvider String  @map("gateway_provider")
  /// Dify workflow id 或网关侧 workflow 标识
  workflowId      String? @map("workflow_id")

  /// 完整输入快照,用于回放 / 审计
  inputSnapshot  Json    @map("input_snapshot")
  /// prompt 模板原文(若有)
  promptTemplate String? @map("prompt_template") @db.Text
  /// 结构化输出(JSON)
  output         Json?
  /// 文本输出(自然语言部分)
  outputText     String? @map("output_text") @db.Text

  /// 关联实体 — 这个调用是为哪个患者/画像/计划做的?
  linkedPatientId String? @map("linked_patient_id") @db.Uuid
  linkedPersonaId String? @map("linked_persona_id") @db.Uuid
  linkedPlanId    String? @map("linked_plan_id") @db.Uuid

  /// 输入 Token 用量
  promptTokens     Int @default(0) @map("prompt_tokens")
  /// 输出 Token 用量
  completionTokens Int @default(0) @map("completion_tokens")
  /// 总 Token 用量
  totalTokens      Int @default(0) @map("total_tokens")
  /// 估算成本(分,1 分 = 0.01 元)
  costCents        Int @default(0) @map("cost_cents")

  /// 状态:running / succeeded / failed / stream_started / stream_completed
  status       String
  /// 失败时的错误消息
  errorMessage String? @map("error_message") @db.Text

  startedAt DateTime  @default(now()) @map("started_at")
  endedAt   DateTime? @map("ended_at")

  @@index([platformId, tenantId, clinicId])
  @@index([agentKind])
  @@index([linkedPatientId])
  @@index([linkedPlanId])
  @@map("agent_invocations")
}

// =============================================================
// 同步引擎运行日志
// =============================================================

/// 同步引擎每次拉取/接收的运行记录,用于追踪进度、定位失败、计量数据量。
model SyncLog {
  id         String @id @default(uuid()) @db.Uuid
  platformId String @map("platform_id")
  tenantId   String @map("tenant_id")

  /// adapter 方向:pull(我们拉)/ push(他们推)/ mock(本地造数据)
  adapterKind String @map("adapter_kind")
  /// 同步资源(patients / diagnoses / treatments / appointments / *)
  resource    String

  /// 状态:running / success / failed / partial
  status       String
  /// 本次拉取前的游标
  cursorBefore String?   @map("cursor_before")
  /// 本次拉取后的游标
  cursorAfter  String?   @map("cursor_after")
  /// 从宿主拉到的条数
  fetched      Int       @default(0)
  /// upsert 到本地快照表的条数
  upserted     Int       @default(0)
  /// 写入 fact 时间轴的事件数
  factsEmitted Int       @default(0) @map("facts_emitted")
  /// 失败条数
  failed       Int       @default(0)
  /// 失败时的错误信息
  errorMessage String?   @map("error_message") @db.Text
  startedAt    DateTime  @default(now()) @map("started_at")
  endedAt      DateTime? @map("ended_at")
  createdAt    DateTime  @default(now()) @map("created_at")

  @@index([platformId, tenantId, resource])
  @@index([startedAt])
  @@map("sync_logs")
}
