// IIOS Interaction Kernel — P1 schema (native portal only).
// Lifted from the Bottom-Up Evolution Atlas §10 kernel DDL. ONLY kernel tables:
// no support / inbox / route / AI tables (those are earned in later phases).

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

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

// ─── Enums (one value per line) ───────────────────────────────────
enum IiosActorKind {
  HUMAN
  AI
  SERVICE
  BOT
  UNKNOWN
}

enum IiosHandleKind {
  PORTAL_USER
  EMAIL
  PHONE
  WHATSAPP
  SLACK
  MATTERMOST
  TELEGRAM
  EXTERNAL_VISITOR
}

enum IiosInteractionKind {
  MESSAGE
  EMAIL
  SYSTEM_NOTICE
  INBOX_WORK
  SUPPORT_CASE
  MEETING_REQUEST
  DIGEST
  SUMMARY
  NOTIFICATION
}

enum IiosMessagePartKind {
  TEXT
  HTML
  MARKDOWN
  MEDIA_REF
  FILE_REF
  VOICE_REF
  LOCATION
  STRUCTURED_JSON
}

enum IiosDeliveryState {
  RECEIVED
  NORMALIZED
  POLICY_HELD
  STORED
  DELIVERED
  READ
  FAILED
  DELETED_LOCAL
  REDACTED
}

enum IiosReceiptKind {
  DELIVERED
  READ
  ACKED
  FAILED
  SUPPRESSED
  RATE_LIMITED
}

enum IiosInboxItemKind {
  NEEDS_REPLY
  NEEDS_REVIEW
  NEEDS_APPROVAL
  MENTION
  SUPPORT_UPDATE
  MEETING_FOLLOWUP
  DIGEST
  SYSTEM_ALERT
  CRM_OWNER_INTEREST
}

enum IiosInboxState {
  OPEN
  SNOOZED
  DONE
  ARCHIVED
  CANCELLED
  STALE
}

enum IiosTemplateChannel {
  EMAIL
  SMS
  INTERNAL
}

enum IiosTicketState {
  NEW
  OPEN
  PENDING_CUSTOMER
  PENDING_INTERNAL
  RESOLVED
  CLOSED
  CANCELLED
}

enum IiosTicketPriority {
  P0
  P1
  P2
  P3
  P4
}

enum IiosCallbackStatus {
  PENDING
  SCHEDULED
  COMPLETED
  MISSED
  CANCELLED
}

enum IiosRouteMode {
  MANUAL
  AUTOMATIC
  HYBRID
  SIMULATION_ONLY
}

enum IiosOutputFormat {
  FORWARD
  THREADED
  DIGEST
  SUMMARY
  TRANSCRIPT
}

enum IiosRouteDecisionState {
  ALLOW
  DENY
  REVIEW
  SUPPRESS
  SIMULATED
}

// ─── AI enums (P7) ────────────────────────────────────────────────

enum IiosAiJobType {
  CLASSIFY
  SUMMARIZE
  EXTRACT
}

enum IiosAiJobStatus {
  PENDING
  RUNNING
  DONE
  FAILED
  ABSTAINED
}

enum IiosAiArtifactType {
  CLASSIFICATION
  SUMMARY
  TRANSCRIPT
  DIGEST
  EXTRACTION
}

enum IiosAiArtifactStatus {
  PROPOSED
  ACCEPTED
  REJECTED
  SUPERSEDED
}

enum IiosAiClaimType {
  MODERATION_FLAG
  EVENT
  FAQ
  INTENT
  PRIORITY
}

enum IiosAiClaimValidation {
  UNVALIDATED
  ACCEPTED
  REJECTED
}

// ─── Calendar / Meeting enums (P8) ────────────────────────────────

enum IiosMeetingType {
  ZOOM
  PHONE
  IN_PERSON
  CALLBACK
  INTERNAL
}

enum IiosMeetingStatus {
  REQUESTED
  SCHEDULED
  CONFIRMED
  CANCELLED
  COMPLETED
  NO_SHOW
}

enum IiosMeetingRequestStatus {
  PENDING
  PROPOSED
  ACCEPTED
  DECLINED
  SCHEDULED
  CANCELLED
}

enum IiosParticipantRole {
  ORGANIZER
  REQUIRED
  OPTIONAL
}

enum IiosAttendanceStatus {
  INVITED
  ACCEPTED
  DECLINED
  TENTATIVE
  ATTENDED
  NO_SHOW
}

enum IiosParticipantVisibility {
  FULL
  LIMITED
  NONE
}

enum IiosConsentStatus {
  UNKNOWN
  GRANTED
  DENIED
  REVOKED
}

enum IiosTranscriptStatus {
  PENDING
  READY
  BLOCKED
}

enum IiosSummaryStatus {
  PROPOSED
  APPROVED
  PUBLISHED
  REJECTED
}

enum IiosActionItemStatus {
  OPEN
  IN_PROGRESS
  DONE
  CANCELLED
}

enum IiosCalendarProviderType {
  SIMULATED
  GOOGLE
  OUTLOOK
  ICS
}

enum IiosCalendarSyncStatus {
  IDLE
  SYNCING
  ERROR
}

enum IiosCalendarEventStatus {
  CONFIRMED
  TENTATIVE
  CANCELLED
}

// ─── Kernel tables ────────────────────────────────────────────────

/// Frozen six-vector scope. orgId + appId are REQUIRED (no global-by-null).
model IiosScope {
  id          String   @id @default(cuid())
  orgId       String
  appId       String
  buId        String?
  tenantId    String?
  workspaceId String?
  projectId   String?
  userScopeId String?
  cellId      String   @default("cell-default") // P9 cell-partition hook
  createdAt   DateTime @default(now())

  sourceHandles IiosSourceHandle[]
  channels      IiosChannel[]
  threads       IiosThread[]
  interactions  IiosInteraction[]
  annotations   IiosInteractionAnnotation[]
  inboxItems    IiosInboxItem[]
  supportQueues IiosSupportQueue[]
  tickets       IiosTicket[]
  callbacks     IiosCallbackRequest[]
  notificationSubscriptions IiosNotificationSubscription[]
  messageTemplates IiosMessageTemplate[]
  providerCredentials IiosProviderCredential[]

  @@index([orgId, appId, tenantId])
}

/// A tenant's own credentials for an egress provider (BYO: Twilio SMS, own SMTP, …).
/// The secret is sealed with AES-256-GCM under the platform key (IIOS_CRED_KEY); only
/// `displayHints` (non-secret, e.g. from-number / SID last-4) is ever read back out.
/// One row per (scope, providerType). Resolved at send time by the channel's provider.
model IiosProviderCredential {
  id           String    @id @default(cuid())
  scopeId      String
  scope        IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  providerType String    // TWILIO_SMS | SMTP | WHATSAPP_CLOUD | …
  cipherText   String    // base64(AES-256-GCM ciphertext of the JSON config)
  iv           String    // base64 nonce
  authTag      String    // base64 GCM auth tag
  keyVersion   Int       @default(1)
  displayHints Json?     // non-secret display fields (fromNumber, sidLast4, …)
  enabled      Boolean   @default(true)
  createdAt    DateTime  @default(now())
  updatedAt    DateTime  @updatedAt

  @@unique([scopeId, providerType])
  @@index([scopeId])
}

/// A channel-specific handle (phone/email/portal user). NOT identity — stays
/// UNVERIFIED until MDM resolves it; no silent merge.
model IiosSourceHandle {
  id                String         @id @default(cuid())
  scopeId           String
  scope             IiosScope      @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  kind              IiosHandleKind
  externalId        String
  displayName       String?
  canonicalEntityId String? // set only when MDM resolves; null = unresolved
  confidence        Float          @default(0)
  verificationState String         @default("UNVERIFIED")
  firstSeenAt       DateTime       @default(now())
  lastSeenAt        DateTime       @default(now())
  metadata          Json?
  redactedAt        DateTime? // set when this subject has been erased (DSR, P9)

  actors IiosActorRef[]

  @@unique([scopeId, kind, externalId])
  @@index([canonicalEntityId])
}

/// The local actor (human / AI / service / unknown) behind an interaction.
model IiosActorRef {
  id                  String           @id @default(cuid())
  kind                IiosActorKind
  sourceHandleId      String?
  sourceHandle        IiosSourceHandle? @relation(fields: [sourceHandleId], references: [id])
  platformPrincipalRef String?
  canonicalEntityId   String?
  displayName         String?
  createdAt           DateTime         @default(now())

  threadsCreated     IiosThread[]            @relation("ThreadCreatedBy")
  participations     IiosThreadParticipant[]
  interactions       IiosInteraction[]
  annotations        IiosInteractionAnnotation[]
  receipts           IiosMessageReceipt[]
  unreadCounters     IiosUnreadCounter[]
  inboxItemsOwned    IiosInboxItem[]
  teamMemberships    IiosSupportTeamMember[]
  ticketsRequested   IiosTicket[]            @relation("TicketRequester")
  ticketsAssigned    IiosTicket[]            @relation("TicketAssignee")
  callbacksRequested IiosCallbackRequest[]
  notificationSubscriptions IiosNotificationSubscription[]
}

/// A configured channel surface (PORTAL in P1).
model IiosChannel {
  id                 String   @id @default(cuid())
  scopeId            String
  scope              IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  channelType        String
  channelName        String
  direction          String   @default("BOTH")
  capabilityContract Json?
  externalRef        String?
  enabled            Boolean  @default(true)
  createdAt          DateTime @default(now())

  threads      IiosThread[]
  interactions IiosInteraction[]

  @@index([scopeId, channelType])
}

/// A conversation/thread on a channel.
model IiosThread {
  id               String    @id @default(cuid())
  scopeId          String
  scope            IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  channelId        String?
  channel          IiosChannel? @relation(fields: [channelId], references: [id])
  threadKind       String    @default("CONVERSATION")
  subject          String?
  externalThreadRef String?
  status           String    @default("OPEN")
  visibilityClass  String    @default("PRIVATE")
  createdByActorId String?
  createdByActor   IiosActorRef? @relation("ThreadCreatedBy", fields: [createdByActorId], references: [id])
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt
  metadata         Json?

  participants   IiosThreadParticipant[]
  interactions   IiosInteraction[]
  unreadCounters IiosUnreadCounter[]
  inboxItems     IiosInboxItem[]
  ticketLinks    IiosTicketThreadLink[]

  @@index([scopeId, status])
}

model IiosThreadParticipant {
  threadId        String
  thread          IiosThread   @relation(fields: [threadId], references: [id], onDelete: Cascade)
  actorId         String
  actor           IiosActorRef @relation(fields: [actorId], references: [id])
  participantRole String       @default("MEMBER")
  muted           Boolean      @default(false)
  joinedAt        DateTime     @default(now())
  leftAt          DateTime?

  @@id([threadId, actorId])
}

/// A device/browser push subscription for an actor (Web Push endpoint + keys).
/// The notification engine delivers to these when the actor is absent.
model IiosNotificationSubscription {
  id         String       @id @default(cuid())
  scopeId    String
  scope      IiosScope    @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  actorId    String
  actor      IiosActorRef @relation(fields: [actorId], references: [id])
  kind       String       @default("webpush")
  endpoint   String       @unique
  p256dh     String
  auth       String
  userAgent  String?
  createdAt  DateTime     @default(now())
  lastSeenAt DateTime     @default(now())

  @@index([actorId])
}

/// The semantic envelope. Idempotent per (scope, idempotencyKey).
model IiosInteraction {
  id                  String              @id @default(cuid())
  scopeId             String
  scope               IiosScope           @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  kind                IiosInteractionKind
  threadId            String?
  thread              IiosThread?         @relation(fields: [threadId], references: [id])
  channelId           String?
  channel             IiosChannel?        @relation(fields: [channelId], references: [id])
  actorId             String?
  actor               IiosActorRef?       @relation(fields: [actorId], references: [id])
  parentInteractionId String?
  parent              IiosInteraction?    @relation("InteractionParent", fields: [parentInteractionId], references: [id])
  children            IiosInteraction[]   @relation("InteractionParent")
  externalId          String?
  idempotencyKey      String
  occurredAt          DateTime            @default(now())
  receivedAt          DateTime            @default(now())
  status              IiosDeliveryState   @default(RECEIVED)
  dataClass           String              @default("internal") // envelope data class; drives retention window (P9)
  policyDecisionRef   String?
  consentReceiptRef   String?
  crreBundleRef       String?
  traceId             String?
  metadata            Json?

  parts             IiosMessagePart[]
  receipts          IiosMessageReceipt[]
  annotations       IiosInteractionAnnotation[]
  inboxItems        IiosInboxItem[]
  ticketsCreatedFrom IiosTicket[]

  @@unique([scopeId, idempotencyKey])
  @@index([threadId, occurredAt])
}

/// A generic annotation an actor attaches to an interaction. Both `annotationType`
/// and `value` are OPAQUE, app-supplied strings the kernel stores and aggregates
/// but never interprets — e.g. the chat app writes type "reaction" / value "👍".
/// The same primitive backs pins, saves, flags, tags later. No chat vocabulary here.
model IiosInteractionAnnotation {
  id                  String          @id @default(cuid())
  scopeId             String
  scope               IiosScope       @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  targetInteractionId String
  target              IiosInteraction @relation(fields: [targetInteractionId], references: [id], onDelete: Cascade)
  actorId             String
  actor               IiosActorRef    @relation(fields: [actorId], references: [id])
  annotationType      String
  value               String
  createdAt           DateTime        @default(now())

  @@unique([scopeId, targetInteractionId, actorId, annotationType, value])
  @@index([targetInteractionId])
}

model IiosMessagePart {
  id             String              @id @default(cuid())
  interactionId  String
  interaction    IiosInteraction     @relation(fields: [interactionId], references: [id], onDelete: Cascade)
  partIndex      Int
  kind           IiosMessagePartKind
  bodyText       String?
  contentRef     String?
  mimeType       String?
  sizeBytes      BigInt?
  checksumSha256 String?
  metadata       Json?

  @@unique([interactionId, partIndex])
}

/// A stored media object (bytes behind the StoragePort). A row is recorded when bytes actually
/// land (MediaService.put), so an orphan sweep can reap objects that no message part references —
/// e.g. a file attached in a composer but never sent. "Referenced" is derived at sweep time from
/// IiosMessagePart.contentRef (no stored flag to drift), after a grace period so in-progress
/// composes are never reaped.
model IiosMediaObject {
  id        String   @id @default(cuid())
  scopeId   String
  objectKey String   @unique
  mime      String
  sizeBytes BigInt
  createdAt DateTime @default(now())

  @@index([createdAt])
}

/// Transactional outbox — written in the same tx as the business row.
model IiosOutboxEvent {
  eventId       String    @id @default(cuid())
  aggregateType String
  aggregateId   String
  eventType     String
  cloudEvent    Json
  partitionKey  String
  status        String    @default("PENDING")
  attempts      Int       @default(0)
  nextAttemptAt DateTime  @default(now())
  createdAt     DateTime  @default(now())
  publishedAt   DateTime?

  @@index([status, nextAttemptAt, createdAt])
}

/// Idempotent-consumer ledger for the outbox relay / downstream consumers.
model IiosProcessedEvent {
  consumerName String
  eventId      String
  processedAt  DateTime @default(now())
  resultHash   String?

  @@id([consumerName, eventId])
}

/// Per-resource retention snapshot (P9). Absolute lifecycle timestamps frozen from the
/// resource's age + policy window; read by the retention sweep. delete = redact-in-place.
model IiosRetentionPolicySnapshot {
  id              String    @id @default(cuid())
  policyKey       String
  scopeSnapshotId String
  targetType      String // 'interaction'
  targetId        String
  dataClass       String
  retainUntil     DateTime?
  archiveAfter    DateTime
  deleteAfter     DateTime
  sourceVersion   String    @default("v1")
  status          String    @default("ACTIVE") // ACTIVE | ARCHIVED | REDACTED
  createdAt       DateTime  @default(now())

  @@unique([targetType, targetId])
  @@index([status, deleteAfter])
  @@index([scopeSnapshotId, status])
}

/// Legal/compliance hold (P9 DSR). An ACTIVE, unexpired hold over a target BLOCKS
/// erasure/retention deletion until an authorised release. Never auto-released.
model IiosComplianceHold {
  id               String    @id @default(cuid())
  scopeId          String
  targetType       String // 'data_subject' | 'message' | 'media'
  targetId         String
  holdReason       String
  appliedByActorId String?
  status           String    @default("ACTIVE") // ACTIVE | RELEASED
  createdAt        DateTime  @default(now())
  expiresAt        DateTime?
  releasedAt       DateTime?

  @@index([scopeId, targetType, targetId, status])
}

/// Projection replay cursor (P9, KG-06). Ordered high-water-mark + rolling checksum per
/// (projection, topic, partition) proving replay reproduces the same projection outcome.
model IiosProjectionCursor {
  id             String   @id @default(cuid())
  projectionName String
  sourceTopic    String
  partitionKey   String
  lastEventId    String
  lastOffset     Int      @default(0)
  checksum       String
  updatedAt      DateTime @updatedAt

  @@unique([projectionName, sourceTopic, partitionKey])
  @@index([partitionKey])
}

/// Idempotent-command ledger (P9). Suppresses duplicate externally-visible mutations:
/// same (scope, command, key) replays the cached response; a different request → conflict.
model IiosIdempotencyCommand {
  id             String    @id @default(cuid())
  scopeId        String?
  commandName    String
  idempotencyKey String
  schemaVersion  String    @default("v1")
  actorRefId     String?
  requestHash    String
  responseHash   String?
  response       Json?
  status         String    @default("IN_FLIGHT") // IN_FLIGHT | COMPLETED | FAILED
  createdAt      DateTime  @default(now())
  completedAt    DateTime?

  @@unique([scopeId, commandName, idempotencyKey])
  @@index([scopeId, status])
}

// ─── P2: messaging — receipts & unread ────────────────────────────

/// Delivery/read receipt for a message interaction, per actor.
model IiosMessageReceipt {
  id                String          @id @default(cuid())
  interactionId     String
  interaction       IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade)
  actorId           String?
  actor             IiosActorRef?   @relation(fields: [actorId], references: [id])
  receiptKind       IiosReceiptKind
  occurredAt        DateTime        @default(now())
  providerReceiptRef String?
  metadata          Json?

  @@unique([interactionId, actorId, receiptKind])
  @@index([interactionId])
}

/// Per-(thread, actor) unread count + last-read marker.
model IiosUnreadCounter {
  threadId             String
  thread               IiosThread       @relation(fields: [threadId], references: [id], onDelete: Cascade)
  actorId              String
  actor                IiosActorRef     @relation(fields: [actorId], references: [id])
  unreadCount          Int              @default(0)
  lastReadInteractionId String?
  updatedAt            DateTime         @updatedAt

  @@id([threadId, actorId])
}

// ─── P3: Inbox (work/awareness surface) ───────────────────────────

/// A durable action/awareness item owned by one actor. Fed by the event
/// projector (deterministic, collapse-per-thread). NOT a thin unread projection.
model IiosInboxItem {
  id                  String            @id @default(cuid())
  scopeId             String
  scope               IiosScope         @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  ownerActorId        String
  ownerActor          IiosActorRef      @relation(fields: [ownerActorId], references: [id])
  kind                IiosInboxItemKind
  state               IiosInboxState    @default(OPEN)
  title               String
  summary             String?
  priority            String            @default("NORMAL")
  dueAt               DateTime?
  sourceInteractionId String?
  sourceInteraction   IiosInteraction?  @relation(fields: [sourceInteractionId], references: [id])
  threadId            String?
  thread              IiosThread?       @relation(fields: [threadId], references: [id])
  traceId             String?
  actionRef           Json?
  policyDecisionRef   String?
  createdAt           DateTime          @default(now())
  updatedAt           DateTime          @updatedAt
  metadata            Json?

  stateHistory IiosInboxItemStateHistory[]

  @@index([ownerActorId, state, priority])
}

/// A reusable message template (email/SMS/in-app). DB is runtime truth; platform defaults are
/// seeded from repo files on boot. Versions are immutable — a change writes a new (higher) version,
/// never edits in place, so a rendered send can always be traced to the exact source it used.
/// scopeId NULL = a platform default; set = a tenant scope's override of the same key. Resolution
/// prefers the scoped row, else the global default. (The global-uniqueness of NULL-scope rows is
/// enforced by a partial unique index added in the migration — Postgres treats NULL as distinct.)
model IiosMessageTemplate {
  id        String              @id @default(cuid())
  scopeId   String?
  scope     IiosScope?          @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  key       String
  channel   IiosTemplateChannel
  locale    String              @default("en")
  version   Int                 @default(1)
  subject   String?
  bodyHtml  String?
  bodyText  String?
  /// Declared variable names — render throws if a declared var is missing (fail loud).
  variables Json?
  active    Boolean             @default(true)
  createdAt DateTime            @default(now())
  updatedAt DateTime            @updatedAt

  @@unique([scopeId, key, channel, locale, version])
  @@index([key, channel, locale, active])
}

/// Audit trail of inbox-item state transitions.
model IiosInboxItemStateHistory {
  id          String          @id @default(cuid())
  inboxItemId String
  inboxItem   IiosInboxItem   @relation(fields: [inboxItemId], references: [id], onDelete: Cascade)
  fromState   IiosInboxState?
  toState     IiosInboxState
  actorId     String?
  reasonCode  String?
  at          DateTime        @default(now())

  @@index([inboxItemId])
}

// ─── P4: Support (specialization over message + inbox) ────────────

/// A support queue; agents (team members) are assigned tickets from it.
model IiosSupportQueue {
  id               String    @id @default(cuid())
  scopeId          String
  scope            IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  name             String
  supportTier      String    @default("REGULAR")
  slaPolicyRef     String? // SLA placeholder (no timer logic until P7)
  assignmentPolicy Json?
  enabled          Boolean   @default(true)
  createdAt        DateTime  @default(now())

  members IiosSupportTeamMember[]
  tickets IiosTicket[]

  @@index([scopeId])
}

/// An agent's membership + capacity in a queue. Agent = an ActorRef that is a member.
model IiosSupportTeamMember {
  queueId           String
  queue             IiosSupportQueue @relation(fields: [queueId], references: [id], onDelete: Cascade)
  actorId           String
  actor             IiosActorRef     @relation(fields: [actorId], references: [id])
  role              String           @default("AGENT")
  maxActive         Int              @default(3)
  activeCount       Int              @default(0)
  availabilityState String           @default("OFFLINE")

  @@id([queueId, actorId])
  @@index([actorId])
}

/// Support work object. Linked many-to-many to threads; the thread stays generic.
model IiosTicket {
  id                      String             @id @default(cuid())
  scopeId                 String
  scope                   IiosScope          @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  queueId                 String?
  queue                   IiosSupportQueue?  @relation(fields: [queueId], references: [id])
  requesterActorId        String?
  requesterActor          IiosActorRef?      @relation("TicketRequester", fields: [requesterActorId], references: [id])
  assignedActorId         String?
  assignedActor           IiosActorRef?      @relation("TicketAssignee", fields: [assignedActorId], references: [id])
  subject                 String
  state                   IiosTicketState    @default(NEW)
  priority                IiosTicketPriority @default(P3)
  issueClass              String?
  issueSubclass           String?
  slaDueAt                DateTime? // SLA placeholder (no timer logic until P7)
  createdFromInteractionId String?
  createdFromInteraction  IiosInteraction?   @relation(fields: [createdFromInteractionId], references: [id])
  traceId                 String?
  createdAt               DateTime           @default(now())
  updatedAt               DateTime           @updatedAt
  metadata                Json?

  threadLinks  IiosTicketThreadLink[]
  stateHistory IiosTicketStateHistory[]
  callbacks    IiosCallbackRequest[]

  @@index([scopeId, state])
  @@index([assignedActorId, state])
}

/// M:N link between tickets and kernel threads (one thread ↔ many tickets, and vice-versa).
model IiosTicketThreadLink {
  ticketId     String
  ticket       IiosTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
  threadId     String
  thread       IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
  relationKind String     @default("PRIMARY")
  linkedAt     DateTime   @default(now())

  @@id([ticketId, threadId])
  @@index([threadId])
}

/// Ticket state-transition audit.
model IiosTicketStateHistory {
  id         String           @id @default(cuid())
  ticketId   String
  ticket     IiosTicket       @relation(fields: [ticketId], references: [id], onDelete: Cascade)
  fromState  IiosTicketState?
  toState    IiosTicketState
  actorId    String?
  reasonCode String?
  at         DateTime         @default(now())

  @@index([ticketId])
}

/// Callback request placeholder (the Meeting module is P8; meetingRef is a stub).
model IiosCallbackRequest {
  id               String             @id @default(cuid())
  scopeId          String
  scope            IiosScope          @relation(fields: [scopeId], references: [id], onDelete: Cascade)
  ticketId         String?
  ticket           IiosTicket?        @relation(fields: [ticketId], references: [id])
  requesterActorId String
  requesterActor   IiosActorRef       @relation(fields: [requesterActorId], references: [id])
  preferChannel    String
  preferredTime    DateTime?
  notes            String?
  status           IiosCallbackStatus @default(PENDING)
  meetingRef       String?
  createdAt        DateTime           @default(now())

  @@index([scopeId, status])
}

// ─── P5: Adapters (anti-corruption layer, simulation mode) ────────

/// Raw inbound provider payload (claim-check / replay). Verified before storing.
model IiosInboundRawEvent {
  id              String   @id @default(cuid())
  channelType     String
  externalEventId String?
  signatureStatus String // VERIFIED | INVALID | UNSIGNED
  status          String   @default("RECEIVED") // RECEIVED | NORMALIZED | REJECTED | FAILED
  headers         Json?
  payload         Json
  traceId         String?
  interactionId   String?
  createdAt       DateTime @default(now())

  @@unique([channelType, externalEventId])
  @@index([status])
}

/// A signed command to send on an external channel — executed by the sandbox sink.
model IiosOutboundCommand {
  id             String                @id @default(cuid())
  channelType    String
  target         String
  payload        Json
  status           String              @default("PENDING") // PENDING | SENT | FAILED | RATE_LIMITED | BLOCKED
  scopeId          String?             // owning tenant scope (P9 tenant fencing + per-tenant quota)
  idempotencyKey   String              @unique
  providerRef      String?
  consentReceiptRef String?            // CMP consent receipt this send went out under (P9)
  // Template provenance (which source produced this send) — null for non-templated sends.
  templateKey      String?
  templateVersion  Int?
  templateLocale   String?
  renderedHash     String?             // sha256 of the rendered content, for replay/audit
  createdAt        DateTime            @default(now())

  attempts IiosDeliveryAttempt[]

  @@index([channelType, status])
}

model IiosDeliveryAttempt {
  id          String              @id @default(cuid())
  commandId   String
  command     IiosOutboundCommand @relation(fields: [commandId], references: [id], onDelete: Cascade)
  attemptNo   Int
  status      String
  providerRef String?
  latencyMs   Int?
  errorCode   String?
  at          DateTime            @default(now())

  @@index([commandId])
}

/// Per-(channelType, key) token/window bucket for outbound throttling.
model IiosRateLimitBucket {
  channelType String
  bucketKey   String
  windowStart DateTime @default(now())
  count       Int      @default(0)
  resetAt     DateTime

  @@id([channelType, bucketKey])
}

// ─── P6: Routing (preview-first community forwarding) ──────────────

/// One origin→destination route rule. Preview-first: SIMULATION_ONLY + disabled
/// + requiresReview by default. Restricted destinations are deny-by-default.
model IiosRouteBinding {
  id                    String           @id @default(cuid())
  scopeId               String
  originChannelType     String
  originRef             String?
  destinationChannelType String
  destinationRef        String?
  restrictionProfile    String? // e.g. CHILD / SENIORS / ADULT / PUBLIC
  mode                  IiosRouteMode    @default(SIMULATION_ONLY)
  outputFormat          IiosOutputFormat @default(FORWARD)
  frequency             String           @default("IMMEDIATE")
  includeAttachments    Boolean          @default(false)
  requiresReview        Boolean          @default(true)
  rulepackVersion       String           @default("v1")
  enabled               Boolean          @default(false)
  createdAt             DateTime         @default(now())

  decisions IiosRouteDecision[]

  @@index([scopeId, originChannelType])
}

/// Per-(interaction, binding) routing decision + rendered preview. No send during simulation.
model IiosRouteDecision {
  id                String                @id @default(cuid())
  interactionId     String
  routeBindingId    String
  routeBinding      IiosRouteBinding      @relation(fields: [routeBindingId], references: [id], onDelete: Cascade)
  decisionState     IiosRouteDecisionState
  reasonCodes       String[]
  previewPayload    Json
  outputFormat      IiosOutputFormat
  rulepackVersion   String
  simulationId      String?
  decidedByActorId  String?
  executed          Boolean               @default(false)
  executedCommandId String?
  createdAt         DateTime              @default(now())

  @@unique([interactionId, routeBindingId])
  @@index([decisionState])
}

/// A deterministic (RULE) or later AI-proposed flag on an interaction. AI can
/// propose but never allow a route (P7).
model IiosModerationFlag {
  id            String   @id @default(cuid())
  interactionId String
  flagType      String
  severity      String   @default("LOW")
  proposedBy    String   @default("RULE")
  confidence    Float?
  explanation   String?
  createdAt     DateTime @default(now())
  resolvedAt    DateTime?

  @@index([interactionId])
}

// ─── AI Interaction SDK tables (P7) — proposal layer, never decides ─────────

/// A scoped, purposed AI work order (classify / summarize / extract). Carries the
/// provenance the critics mandate (input_scope_hash, policy_decision_id, purpose,
/// replay_version, traceId, idempotencyKey). Idempotent per key.
model IiosAiJob {
  id               String          @id @default(cuid())
  scopeId          String
  jobType          IiosAiJobType
  interactionId    String?
  inputWindowRef   String
  purpose          String
  status           IiosAiJobStatus @default(PENDING)
  modelPolicyRef   String          @default("golden-v1")
  policyDecisionRef String?
  inputScopeHash   String
  replayVersion    String          @default("v1")
  abstentionReason String?
  traceId          String?
  idempotencyKey   String          @unique
  createdAt        DateTime        @default(now())

  artifacts IiosAiArtifact[]

  @@index([scopeId, jobType])
  @@index([interactionId])
}

/// Model/prompt/tool execution metadata + cost. The budget governor (KG-12) sums
/// costUnits per scope. In P7 provider='fake' (deterministic golden model).
model IiosAiModelRun {
  id            String   @id @default(cuid())
  scopeId       String
  provider      String   @default("fake")
  model         String
  modelVersion  String
  promptVersion String
  toolPolicyRef String?
  tokensIn      Int      @default(0)
  tokensOut     Int      @default(0)
  costUnits     Int      @default(0)
  latencyMs     Int      @default(0)
  createdAt     DateTime @default(now())

  artifacts IiosAiArtifact[]
  toolCalls IiosAiToolCall[]

  @@index([scopeId])
}

/// A durable AI proposal/output. NEVER committed truth by itself — status starts
/// PROPOSED and only a human accept (or a deterministic gate) advances it.
model IiosAiArtifact {
  id               String               @id @default(cuid())
  jobId            String
  job              IiosAiJob            @relation(fields: [jobId], references: [id], onDelete: Cascade)
  modelRunId       String
  modelRun         IiosAiModelRun       @relation(fields: [modelRunId], references: [id], onDelete: Cascade)
  artifactType     IiosAiArtifactType
  status           IiosAiArtifactStatus @default(PROPOSED)
  confidence       Float?
  contentRef       Json
  explanationRef   String?
  abstentionReason String?
  acceptedByActorId String?
  createdAt        DateTime             @default(now())

  claims   IiosAiClaim[]
  evidence IiosAiEvidenceLink[]

  @@index([jobId])
  @@index([status])
}

/// A structured extracted claim (event date, intent, priority, FAQ, moderation
/// flag). validationStatus stays UNVALIDATED until a human/deterministic accept.
model IiosAiClaim {
  id               String                @id @default(cuid())
  artifactId       String
  artifact         IiosAiArtifact        @relation(fields: [artifactId], references: [id], onDelete: Cascade)
  claimType        IiosAiClaimType
  claimJson        Json
  confidence       Float?
  validationStatus IiosAiClaimValidation @default(UNVALIDATED)
  acceptedByRef    String?
  createdAt        DateTime              @default(now())

  @@index([artifactId])
}

/// Source citation for an AI claim/artifact (message / segment / KB chunk).
model IiosAiEvidenceLink {
  id             String         @id @default(cuid())
  artifactId     String
  artifact       IiosAiArtifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
  sourceType     String
  sourceId       String
  spanRef        String?
  relevanceScore Float?
  createdAt      DateTime       @default(now())

  @@index([artifactId])
}

/// Tool calls made by an agent under MCP/tool policy. Schema now; exercised when a
/// real provider + tools land in P9.
model IiosAiToolCall {
  id            String         @id @default(cuid())
  modelRunId    String
  modelRun      IiosAiModelRun @relation(fields: [modelRunId], references: [id], onDelete: Cascade)
  toolName      String
  toolInputHash String
  toolOutputRef String?
  policyRefId   String?
  status        String         @default("PENDING")
  createdAt     DateTime       @default(now())

  @@index([modelRunId])
}

/// Reference to a vector embedding record/index; does not expose text. Schema now;
/// exercised when RAG lands in P9.
model IiosEmbeddingRef {
  id             String   @id @default(cuid())
  sourceType     String
  sourceId       String
  embeddingModel String
  vectorStoreRef String
  embeddingHash  String
  version        String
  createdAt      DateTime @default(now())

  @@unique([sourceType, sourceId, version])
}

// ─── Calendar / Meeting SDK tables (P8) — interaction specialization ────────

/// A request to schedule/reschedule/cancel a meeting. Genesis: a P4 callback
/// (callbackRequestId), an accepted P7 AI EVENT claim (sourceClaimId), or direct.
model IiosMeetingRequest {
  id                String                   @id @default(cuid())
  scopeId           String
  interactionId     String?
  requestedByActorId String
  targetActorId     String?
  requestedWindowJson Json
  meetingType       IiosMeetingType          @default(CALLBACK)
  status            IiosMeetingRequestStatus @default(PENDING)
  resultingMeetingId String?
  callbackRequestId String?
  sourceClaimId     String?
  createdAt         DateTime                 @default(now())

  @@index([scopeId, status])
}

/// A meeting: Zoom / phone / in-person / callback / internal. Owns calendar
/// semantics; a message is only a related artifact.
model IiosMeeting {
  id               String            @id @default(cuid())
  scopeId          String
  interactionId    String?
  calendarEventId  String?
  meetingType      IiosMeetingType
  providerRef      String?
  joinUrlRef       String?
  status           IiosMeetingStatus @default(REQUESTED)
  organizerActorId String
  threadId         String?
  title            String
  startAt          DateTime?
  endAt            DateTime?
  timezone         String            @default("UTC")
  createdAt        DateTime          @default(now())

  participants IiosMeetingParticipant[]
  transcripts  IiosMeetingTranscript[]
  summaries    IiosMeetingSummary[]
  actionItems  IiosMeetingActionItem[]

  @@index([scopeId, status])
}

/// Attendee with role, attendance, per-attendee recording consent + visibility.
/// The consent + visibility model is the P8 safety spine (KG-14 / Scenario 6).
model IiosMeetingParticipant {
  id               String                    @id @default(cuid())
  meetingId        String
  meeting          IiosMeeting               @relation(fields: [meetingId], references: [id], onDelete: Cascade)
  actorRefId       String?
  sourceHandleId   String?
  role             IiosParticipantRole       @default(REQUIRED)
  attendanceStatus IiosAttendanceStatus      @default(INVITED)
  recordingConsent IiosConsentStatus         @default(UNKNOWN)
  visibility       IiosParticipantVisibility @default(FULL)
  createdAt        DateTime                  @default(now())

  @@unique([meetingId, actorRefId])
  @@index([meetingId])
}

/// Transcript artifact metadata. Created only after the consent gate passes;
/// otherwise status=BLOCKED with no segments.
model IiosMeetingTranscript {
  id           String               @id @default(cuid())
  meetingId    String
  meeting      IiosMeeting          @relation(fields: [meetingId], references: [id], onDelete: Cascade)
  sourceType   String               @default("SIMULATED")
  transcriptRef String
  consentRefId String?
  status       IiosTranscriptStatus @default(PENDING)
  language     String               @default("en")
  createdAt    DateTime             @default(now())

  segments IiosTranscriptSegment[]

  @@index([meetingId])
}

/// Speaker-attributed transcript segment.
model IiosTranscriptSegment {
  id            String                @id @default(cuid())
  transcriptId  String
  transcript    IiosMeetingTranscript @relation(fields: [transcriptId], references: [id], onDelete: Cascade)
  segmentNo     Int
  speakerActorId String?
  startMs       Int
  endMs         Int
  contentRef    String
  confidence    Float?
  createdAt     DateTime              @default(now())

  @@index([transcriptId])
}

/// AI or human meeting summary; links to a P7 ai_artifact.
model IiosMeetingSummary {
  id               String            @id @default(cuid())
  meetingId        String
  meeting          IiosMeeting       @relation(fields: [meetingId], references: [id], onDelete: Cascade)
  aiArtifactId     String?
  summaryType      String            @default("AI")
  status           IiosSummaryStatus @default(PROPOSED)
  approvedByActorId String?
  publishedAt      DateTime?
  createdAt        DateTime          @default(now())

  @@index([meetingId])
}

/// Generic task/follow-up (Inbox-owned source of truth), derived from meetings,
/// support, messages or CRM workflows.
model IiosActionItem {
  id                 String               @id @default(cuid())
  scopeId            String
  sourceInteractionId String?
  sourceMessageId    String?
  ownerActorId       String?
  ownerTeamId        String?
  title              String
  status             IiosActionItemStatus @default(OPEN)
  dueAt              DateTime?
  completedAt        DateTime?
  createdAt          DateTime             @default(now())

  meetingLinks IiosMeetingActionItem[]

  @@index([scopeId, status])
}

/// Meeting-derived action item linked to a generic action_item.
model IiosMeetingActionItem {
  id             String               @id @default(cuid())
  meetingId      String
  meeting        IiosMeeting          @relation(fields: [meetingId], references: [id], onDelete: Cascade)
  actionItemId   String
  actionItem     IiosActionItem       @relation(fields: [actionItemId], references: [id], onDelete: Cascade)
  ownerActorId   String?
  sourceSegmentId String?
  status         IiosActionItemStatus @default(OPEN)
  createdAt      DateTime             @default(now())

  @@unique([meetingId, actionItemId])
  @@index([meetingId])
}

/// Connected calendar/provider account or service mailbox; stores refs, not secrets.
model IiosCalendarProviderAccount {
  id                 String                   @id @default(cuid())
  scopeId            String
  actorRefId         String?
  providerType       IiosCalendarProviderType @default(SIMULATED)
  accountHandleToken String
  secretRef          String?
  syncStatus         IiosCalendarSyncStatus   @default(IDLE)
  createdAt          DateTime                 @default(now())

  events  IiosCalendarEvent[]
  cursors IiosCalendarSyncCursor[]

  @@index([scopeId])
}

/// Imported or created calendar event with policy-scoped metadata.
model IiosCalendarEvent {
  id                String                  @id @default(cuid())
  scopeId           String
  providerAccountId String?
  providerAccount   IiosCalendarProviderAccount? @relation(fields: [providerAccountId], references: [id], onDelete: Cascade)
  externalEventId   String?
  title             String
  startAt           DateTime
  endAt             DateTime
  status            IiosCalendarEventStatus @default(CONFIRMED)
  visibilityClass   String                  @default("internal")
  interactionId     String?
  createdAt         DateTime                @default(now())

  @@unique([providerAccountId, externalEventId])
  @@index([scopeId])
}

/// Availability / scheduling constraints for an actor or team.
model IiosAvailabilityWindow {
  id               String   @id @default(cuid())
  scopeId          String
  actorRefId       String?
  teamProjectionId String?
  startAt          DateTime
  endAt            DateTime
  source           String   @default("USER")
  recurrenceJson   Json?
  status           String   @default("ACTIVE")
  createdAt        DateTime @default(now())

  @@index([scopeId, actorRefId])
}

/// Incremental sync cursor / replay position for a provider calendar.
model IiosCalendarSyncCursor {
  id                String                      @id @default(cuid())
  providerAccountId String
  providerAccount   IiosCalendarProviderAccount @relation(fields: [providerAccountId], references: [id], onDelete: Cascade)
  externalCursor    String?
  lastSyncAt        DateTime?
  status            IiosCalendarSyncStatus      @default(IDLE)
  errorCode         String?
  createdAt         DateTime                    @default(now())

  @@unique([providerAccountId])
}

// ─── Observability (P9) ───────────────────────────────────────────

/// Binds an IIOS action to its trace / actor / resource for cross-service causality
/// and "replay a decision" (mandated iios_audit_link). Written at decision points.
model IiosAuditLink {
  id            String   @id @default(cuid())
  traceId       String?
  correlationId String?
  scopeId       String?
  actorRefId    String?
  action        String
  resourceType  String
  resourceId    String
  createdAt     DateTime @default(now())

  @@index([traceId])
  @@index([resourceType, resourceId])
  @@index([scopeId])
}

/// Dead-letter / replay item (P9, mandated dlq_item). A consumer/relay failure is
/// quarantined here (never silently swallowed — KG-13) and can be replayed by ops.
model IiosDlqItem {
  id           String    @id @default(cuid())
  sourceType   String    // 'projection' | 'outbox' | 'outbound' | ...
  sourceId     String
  failureStage String
  errorCode    String?
  retryCount   Int       @default(0)
  payloadRef   String?   // the outbox eventId to re-dispatch
  consumerName String?
  scopeId      String?   // tenant quarantine
  status       String    @default("QUARANTINED") // QUARANTINED | RESOLVED | ARCHIVED
  createdAt    DateTime  @default(now())
  lastFailedAt DateTime  @default(now())
  resolvedAt   DateTime?

  @@unique([consumerName, sourceId])
  @@index([scopeId, status])
}

// ─── Trust plane (July 12) — context attestation ──────────────────
// Registered clients/BFFs/adapters allowed to call IIOS and attest context on
// behalf of an app. A valid actor token is NOT enough; the caller must present a
// context attestation signed by one of these registered clients.
model IiosClientRegistry {
  clientId      String   @id
  clientType    String   // APPSHELL | SUPPORT_BFF | ADAPTER | CALENDAR | SERVICE
  ownerService  String?
  allowedAppIds String[] // which app_ids this client may attest for
  attestSecret  String?  // dev: shared HS256 signing key (AppShell's key stand-in)
  jwksUri       String?  // prod: verify the attestation signature via JWKS
  spiffeId      String?  // prod: workload identity for mTLS proof
  status        String   @default("ACTIVE") // ACTIVE | DISABLED
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

// Single-use nonce ledger: once a context attestation's nonce is seen it can
// never be replayed. Reserve-if-absent (unique PK) makes the check atomic.
model IiosAttestationNonce {
  nonce       String   @id
  clientId    String
  expiresAt   DateTime
  firstSeenAt DateTime @default(now())

  @@index([expiresAt])
}
