datasource db {
  provider   = "postgresql"
  url        = env("DATABASE_URL")
  directUrl  = env("DIRECT_URL")
  extensions = [pgvector(map: "vector")]
}

generator client {
  provider               = "prisma-client-py"
  recursive_type_depth   = -1
  interface              = "asyncio"
  previewFeatures        = ["views", "fullTextSearch", "postgresqlExtensions"]
  partial_type_generator = "backend/data/partial_types.py"
}

// User model to mirror Auth provider users
model User {
  id               String   @id // This should match the Supabase user ID
  email            String   @unique
  emailVerified    Boolean  @default(true)
  name             String?
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt
  metadata         Json     @default("{}")
  integrations     String   @default("")
  stripeCustomerId String?
  topUpConfig      Json?

  maxEmailsPerDay              Int     @default(3)
  notifyOnAgentRun             Boolean @default(true)
  notifyOnZeroBalance          Boolean @default(true)
  notifyOnLowBalance           Boolean @default(true)
  notifyOnBlockExecutionFailed Boolean @default(true)
  notifyOnContinuousAgentError Boolean @default(true)
  notifyOnDailySummary         Boolean @default(true)
  notifyOnWeeklySummary        Boolean @default(true)
  notifyOnMonthlySummary       Boolean @default(true)
  notifyOnAgentApproved        Boolean @default(true)
  notifyOnAgentRejected        Boolean @default(true)

  timezone String @default("not-set")

  subscriptionTier SubscriptionTier @default(NO_TIER)

  // Relations

  AgentGraphs          AgentGraph[]
  AgentGraphExecutions AgentGraphExecution[]
  AnalyticsDetails     AnalyticsDetails[]
  AnalyticsMetrics     AnalyticsMetrics[]
  CreditTransactions   CreditTransaction[]
  UserBalance          UserBalance?
  ChatSessions         ChatSession[]
  AgentPresets         AgentPreset[]
  LibraryAgents        LibraryAgent[]
  LibraryFolders       LibraryFolder[]

  Profile               Profile[]
  UserOnboarding        UserOnboarding?
  CoPilotUnderstanding  CoPilotUnderstanding?
  BuilderSearchHistory  BuilderSearchHistory[]
  StoreListings         StoreListing[]
  StoreListingReviews   StoreListingReview[]
  StoreVersionsReviewed StoreListingVersion[]
  APIKeys               APIKey[]
  IntegrationWebhooks   IntegrationWebhook[]
  NotificationBatches   UserNotificationBatch[]
  PushSubscriptions     PushSubscription[]
  PendingHumanReviews   PendingHumanReview[]
  Workspace             UserWorkspace?
  PlatformCostLogs      PlatformCostLog[]

  // OAuth Provider relations
  OAuthApplications       OAuthApplication[]
  OAuthAuthorizationCodes OAuthAuthorizationCode[]
  OAuthAccessTokens       OAuthAccessToken[]
  OAuthRefreshTokens      OAuthRefreshToken[]

  // Platform bot linking
  PlatformLinks     PlatformLink[]
  PlatformUserLinks PlatformUserLink[]
}

enum ChatSessionStatus {
  idle
  queued
  running
}

enum SubscriptionTier {
  NO_TIER
  BASIC
  PRO
  MAX
  BUSINESS
  ENTERPRISE
}

enum OnboardingStep {
  // Introductory onboarding (Library)
  WELCOME
  USAGE_REASON
  INTEGRATIONS
  AGENT_CHOICE
  AGENT_NEW_RUN
  AGENT_INPUT
  CONGRATS
  // First Wins
  VISIT_COPILOT
  GET_RESULTS
  MARKETPLACE_VISIT
  MARKETPLACE_ADD_AGENT
  MARKETPLACE_RUN_AGENT
  BUILDER_SAVE_AGENT
  // Consistency Challenge
  RE_RUN_AGENT
  SCHEDULE_AGENT
  RUN_AGENTS
  RUN_3_DAYS
  // The Pro Playground
  TRIGGER_WEBHOOK
  RUN_14_DAYS
  RUN_AGENTS_100
  // No longer rewarded but exist for analytical purposes
  BUILDER_OPEN
  BUILDER_RUN_AGENT
}

model UserOnboarding {
  id        String    @id @default(uuid())
  createdAt DateTime  @default(now())
  updatedAt DateTime? @updatedAt

  completedSteps                OnboardingStep[] @default([])
  walletShown                   Boolean          @default(false)
  notified                      OnboardingStep[] @default([])
  rewardedFor                   OnboardingStep[] @default([])
  usageReason                   String?
  integrations                  String[]         @default([])
  otherIntegrations             String?
  selectedStoreListingVersionId String?
  agentInput                    Json?
  onboardingAgentExecutionId    String?
  agentRuns                     Int              @default(0)
  lastRunAt                     DateTime?
  consecutiveRunDays            Int              @default(0)

  userId String @unique
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model CoPilotUnderstanding {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  userId String @unique
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  data Json?

  @@index([userId])
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////   USER WORKSPACE TABLES   /////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////

// User's persistent file storage workspace
model UserWorkspace {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  userId String @unique
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  Files UserWorkspaceFile[]

  @@index([userId])
}

// Individual files in a user's workspace
model UserWorkspaceFile {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  workspaceId String
  Workspace   UserWorkspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)

  // File metadata
  name        String // User-visible filename
  path        String // Virtual path (e.g., "/documents/report.pdf")
  storagePath String // Actual GCS or local storage path
  mimeType    String
  sizeBytes   BigInt
  checksum    String? // SHA256 for integrity

  // File state
  isDeleted Boolean   @default(false)
  deletedAt DateTime?

  metadata Json @default("{}")

  SharedExecutionFiles SharedExecutionFile[]
  SharedChatFiles      SharedChatFile[]

  @@unique([workspaceId, path])
  @@index([workspaceId, isDeleted])
}

// Tracks which workspace files are exposed via a shared execution.
// Created when sharing is enabled, deleted when sharing is disabled.
// The public file download endpoint validates against this table.
model SharedExecutionFile {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())

  executionId String
  Execution   AgentGraphExecution @relation(fields: [executionId], references: [id], onDelete: Cascade)

  fileId String
  File   UserWorkspaceFile @relation(fields: [fileId], references: [id], onDelete: Cascade)

  shareToken String

  @@unique([shareToken, fileId])
  @@index([shareToken])
  @@index([executionId])
}

model BuilderSearchHistory {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  searchQuery String
  filter      String[] @default([])
  byCreator   String[] @default([])

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////   CHAT SESSION TABLES   ///////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////

model ChatSession {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  userId      String
  User        User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  // Session metadata
  title       String?
  credentials Json    @default("{}") // Map of provider -> credential metadata

  // Rate limiting counters (stored as JSON maps)
  successfulAgentRuns      Json @default("{}") // Map of graph_id -> count
  successfulAgentSchedules Json @default("{}") // Map of graph_id -> count

  // Extensible session metadata (typed via ChatSessionMetadata in Python).
  // Avoids DB migrations for each new flag (e.g. dry_run, future fields).
  metadata Json @default("{}")

  // Usage tracking
  totalPromptTokens     Int @default(0)
  totalCompletionTokens Int @default(0)

  // Session lifecycle for the per-user soft running cap + cross-session
  // queue.  ``idle`` (DEFAULT, no turn in flight), ``queued`` (waiting
  // for a running slot to free), ``running`` (a turn is currently
  // being processed).  ``updatedAt`` doubles as the status-change
  // timestamp (every transition touches the row).
  chatStatus ChatSessionStatus @default(idle)

  Messages ChatMessage[]

  // Sharing fields — mirrors AgentGraphExecution sharing
  isShared   Boolean   @default(false)
  shareToken String?   @unique
  sharedAt   DateTime?

  // When true (set at share-enable time), any run_agent execution in
  // this chat is automatically linked into the share — both the
  // existing history at enable-time and any new runs that land while
  // the share is active.  When false, no executions are linked even
  // if the chat itself is shared (cards in the viewer render in their
  // "execution not shared" state).
  autoShareExecutions Boolean @default(false)

  SharedChatFiles  SharedChatFile[]
  ChatLinkedShares ChatLinkedShare[]

  // Single compound index covers (a) the cap-count (count by userId +
  // chatStatus), (b) the queue-list ORDER BY updatedAt asc, AND (c) the
  // sidebar list (find_many WHERE userId ORDER BY updatedAt desc).
  // For (c) Postgres scans the per-userId index range across all
  // chatStatus sub-buckets and sorts by updatedAt in memory — fine at
  // typical per-user N (≤100s) and not worth a redundant 2-col index.
  // The migration drops the previously-existing (userId, updatedAt)
  // index that this one supersedes.
  @@index([userId, chatStatus, updatedAt], name: "ChatSession_user_status_idx")
  @@index([shareToken])
}

// Tracks which workspace files are exposed via a shared chat session.
// Same allowlist defense as SharedExecutionFile: enumeration-resistant
// downloads via uniform 404 + single allowlist lookup.
model SharedChatFile {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())

  sessionId String
  Session   ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)

  fileId String
  File   UserWorkspaceFile @relation(fields: [fileId], references: [id], onDelete: Cascade)

  shareToken String

  @@unique([shareToken, fileId])
  @@index([shareToken])
  @@index([sessionId])
}

// Links a shared ChatSession to AgentGraphExecution rows that were
// opted-in at share time so the public viewer can resolve drill-ins.
// On chat-share revoke, only executions whose ``sharedVia=CHAT_LINK``
// (no independent user share) get their share state cleared.
model ChatLinkedShare {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())

  sessionId String
  Session   ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)

  executionId String
  Execution   AgentGraphExecution @relation(fields: [executionId], references: [id], onDelete: Cascade)

  @@unique([sessionId, executionId])
  @@index([sessionId])
  @@index([executionId])
}

model ChatMessage {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())

  sessionId String
  Session   ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)

  // Message content
  role         String // "user", "assistant", "system", "tool", "function"
  content      String?
  name         String?
  toolCallId   String?
  refusal      String?
  toolCalls    Json? // List of tool calls for assistant messages
  functionCall Json? // Deprecated but kept for compatibility

  // Ordering within session
  sequence   Int
  durationMs Int? // Wall-clock milliseconds for this assistant turn

  // Per-row JSONB metadata bag.  Currently holds the dispatcher's
  // submit-time payload (file_ids, mode, model, permissions, context,
  // request_arrival_at) on the user row that triggered a queued turn,
  // so a later promotion can replay the original turn faithfully.
  // Generic so future per-row state can land here without a migration.
  metadata Json?

  @@unique([sessionId, sequence])
}

// This model describes the Agent Graph/Flow (Multi Agent System).
model AgentGraph {
  id        String    @default(uuid())
  version   Int       @default(1)
  createdAt DateTime  @default(now())
  updatedAt DateTime? @updatedAt

  name                    String?
  description             String?
  instructions            String?
  recommendedScheduleCron String?

  isActive Boolean @default(true)

  // Link to User model
  userId String
  // FIX: Do not cascade delete the agent when the user is deleted
  // This allows us to delete user data with deleting the agent which maybe in use by other users
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  forkedFromId      String?
  forkedFromVersion Int?
  forkedFrom        AgentGraph?  @relation("AgentGraphForks", fields: [forkedFromId, forkedFromVersion], references: [id, version])
  forks             AgentGraph[] @relation("AgentGraphForks")

  Nodes      AgentNode[]
  Executions AgentGraphExecution[]

  Presets              AgentPreset[]
  LibraryAgents        LibraryAgent[]
  StoreListingVersions StoreListingVersion[]

  @@id(name: "graphVersionId", [id, version])
  @@index([userId, isActive, id, version])
  @@index([forkedFromId, forkedFromVersion])
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////   USER SPECIFIC DATA   ////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////

// An AgentPrest is an Agent + User Configuration of that agent.
// For example, if someone has created a weather agent and they want to set it up to
// Inform them of extreme weather warnings in Texas, the agent with the configuration to set it to
// monitor texas, along with the cron setup or webhook tiggers, is an AgentPreset
model AgentPreset {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  name        String
  description String

  // For agents that can be triggered by webhooks or cronjob
  // This bool allows us to disable a configured agent without deleting it
  isActive Boolean @default(true)

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  agentGraphId      String
  agentGraphVersion Int
  AgentGraph        AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Restrict)

  InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
  Executions   AgentGraphExecution[]

  // For webhook-triggered agents: reference to the webhook that triggers the agent
  webhookId String?
  Webhook   IntegrationWebhook? @relation(fields: [webhookId], references: [id])

  isDeleted Boolean @default(false)

  @@index([userId])
  @@index([agentGraphId, agentGraphVersion])
  @@index([webhookId])
}

enum NotificationType {
  AGENT_RUN
  ZERO_BALANCE
  LOW_BALANCE
  BLOCK_EXECUTION_FAILED
  CONTINUOUS_AGENT_ERROR
  DAILY_SUMMARY
  WEEKLY_SUMMARY
  MONTHLY_SUMMARY
  REFUND_REQUEST
  REFUND_PROCESSED
  AGENT_APPROVED
  AGENT_REJECTED
}

model NotificationEvent {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  UserNotificationBatch   UserNotificationBatch? @relation(fields: [userNotificationBatchId], references: [id])
  userNotificationBatchId String?

  type NotificationType
  data Json

  @@index([userNotificationBatchId])
}

model UserNotificationBatch {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  type NotificationType

  Notifications NotificationEvent[]

  // Each user can only have one batch of a notification type at a time
  @@unique([userId, type])
}

model PushSubscription {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  endpoint String
  p256dh   String
  auth     String

  userAgent    String?
  failCount    Int       @default(0)
  lastFailedAt DateTime?

  @@unique([userId, endpoint])
}

// For the library page
// It is a user controlled list of agents, that they will see in their library
model LibraryAgent {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  imageUrl String?

  agentGraphId      String
  agentGraphVersion Int
  AgentGraph        AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Restrict)

  creatorId String?
  Creator   Profile? @relation(fields: [creatorId], references: [id])

  folderId String?
  Folder   LibraryFolder? @relation(fields: [folderId], references: [id], onDelete: Restrict)

  useGraphIsActiveVersion Boolean @default(false)

  isFavorite      Boolean @default(false)
  isCreatedByUser Boolean @default(false)
  isHidden        Boolean @default(false)
  isArchived      Boolean @default(false)
  isDeleted       Boolean @default(false)

  settings Json @default("{}")

  @@unique([userId, agentGraphId, agentGraphVersion])
  @@index([agentGraphId, agentGraphVersion])
  @@index([creatorId])
  @@index([folderId])
}

model LibraryFolder {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  name  String
  icon  String?
  color String?

  parentId String?
  Parent   LibraryFolder?  @relation("FolderHierarchy", fields: [parentId], references: [id], onDelete: Cascade)
  Children LibraryFolder[] @relation("FolderHierarchy")

  isDeleted Boolean @default(false)

  LibraryAgents LibraryAgent[]

  @@unique([userId, parentId, name]) // Name unique per parent per user
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////   AGENT DEFINITION AND EXECUTION TABLES    ////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////

// This model describes a single node in the Agent Graph/Flow (Multi Agent System).
model AgentNode {
  id String @id @default(uuid())

  agentBlockId String
  AgentBlock   AgentBlock @relation(fields: [agentBlockId], references: [id], onUpdate: Cascade)

  agentGraphId      String
  agentGraphVersion Int        @default(1)
  AgentGraph        AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Cascade)

  // List of consumed input, that the parent node should provide.
  Input AgentNodeLink[] @relation("AgentNodeSink")

  // List of produced output, that the child node should be executed.
  Output AgentNodeLink[] @relation("AgentNodeSource")

  constantInput Json @default("{}")

  // For webhook-triggered blocks: reference to the webhook that triggers the node
  webhookId String?
  Webhook   IntegrationWebhook? @relation(fields: [webhookId], references: [id])

  metadata Json @default("{}")

  Executions AgentNodeExecution[]

  @@index([agentGraphId, agentGraphVersion])
  @@index([agentBlockId])
  @@index([webhookId])
}

// This model describes the link between two AgentNodes.
model AgentNodeLink {
  id String @id @default(uuid())

  // Output of a node is connected to the source of the link.
  agentNodeSourceId String
  AgentNodeSource   AgentNode @relation("AgentNodeSource", fields: [agentNodeSourceId], references: [id], onDelete: Cascade)
  sourceName        String

  // Input of a node is connected to the sink of the link.
  agentNodeSinkId String
  AgentNodeSink   AgentNode @relation("AgentNodeSink", fields: [agentNodeSinkId], references: [id], onDelete: Cascade)
  sinkName        String

  // Default: the data coming from the source can only be consumed by the sink once, Static: input data will be reused.
  isStatic Boolean @default(false)

  @@index([agentNodeSourceId])
  @@index([agentNodeSinkId])
}

// This model describes a component that will be executed by the AgentNode.
model AgentBlock {
  id   String @id @default(uuid())
  name String @unique

  // We allow a block to have multiple types of input & output.
  // Serialized object-typed `jsonschema` with top-level properties as input/output name.
  inputSchema  String
  outputSchema String

  // Block description synced from Python block definitions.
  description          String?
  // LLM-optimized description for agent generation (concise, actionable).
  optimizedDescription String?

  // Prisma requires explicit back-references.
  ReferencedByAgentNode AgentNode[]
}

// This model describes the status of an AgentGraphExecution or AgentNodeExecution.
enum AgentExecutionStatus {
  INCOMPLETE
  QUEUED
  RUNNING
  COMPLETED
  TERMINATED
  FAILED
  REVIEW
}

// This model describes the execution of an AgentGraph.
model AgentGraphExecution {
  id        String    @id @default(uuid())
  createdAt DateTime  @default(now())
  updatedAt DateTime? @updatedAt
  startedAt DateTime?
  endedAt   DateTime?

  isDeleted Boolean @default(false)

  executionStatus AgentExecutionStatus @default(COMPLETED)

  agentGraphId      String
  agentGraphVersion Int        @default(1)
  AgentGraph        AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version], onDelete: Cascade)

  agentPresetId String?
  AgentPreset   AgentPreset? @relation(fields: [agentPresetId], references: [id])

  inputs           Json?
  credentialInputs Json?
  nodesInputMasks  Json?

  NodeExecutions AgentNodeExecution[]

  // Link to User model -- Executed by this user
  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  stats Json?

  // Parent-child execution tracking for nested agent graphs
  parentGraphExecutionId String?
  ParentExecution        AgentGraphExecution?  @relation("ParentChildExecution", fields: [parentGraphExecutionId], references: [id], onDelete: SetNull)
  ChildExecutions        AgentGraphExecution[] @relation("ParentChildExecution")

  // Sharing fields
  isShared             Boolean               @default(false)
  shareToken           String?               @unique
  sharedAt             DateTime?
  // Provenance: USER = explicitly shared by owner, CHAT_LINK = enabled
  // as part of a ChatSession share.  Drives cascade-revoke logic so that
  // user-initiated shares survive chat-share revocation.
  sharedVia            SharedVia?
  SharedExecutionFiles SharedExecutionFile[]
  ChatLinkedShares     ChatLinkedShare[]

  @@index([agentGraphId, agentGraphVersion])
  @@index([userId, isDeleted, createdAt])
  @@index([createdAt])
  @@index([agentPresetId])
  @@index([shareToken])
  @@index([parentGraphExecutionId])
}

enum SharedVia {
  USER
  CHAT_LINK
}

// This model describes the execution of an AgentNode.
model AgentNodeExecution {
  id String @id @default(uuid())

  agentGraphExecutionId String
  GraphExecution        AgentGraphExecution @relation(fields: [agentGraphExecutionId], references: [id], onDelete: Cascade)

  agentNodeId String
  Node        AgentNode @relation(fields: [agentNodeId], references: [id], onDelete: Cascade)

  Input  AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionInput")
  Output AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionOutput")

  executionStatus AgentExecutionStatus @default(COMPLETED)
  executionData   Json?
  addedTime       DateTime             @default(now())
  queuedTime      DateTime?
  startedTime     DateTime?
  endedTime       DateTime?

  stats Json?

  @@index([agentGraphExecutionId, agentNodeId, executionStatus])
  @@index([agentNodeId, executionStatus])
  @@index([addedTime, queuedTime])
}

// This model describes the output of an AgentNodeExecution.
model AgentNodeExecutionInputOutput {
  id String @id @default(uuid())

  name String
  data Json?
  time DateTime @default(now())

  // Prisma requires explicit back-references.
  referencedByInputExecId  String?
  ReferencedByInputExec    AgentNodeExecution? @relation("AgentNodeExecutionInput", fields: [referencedByInputExecId], references: [id], onDelete: Cascade)
  referencedByOutputExecId String?
  ReferencedByOutputExec   AgentNodeExecution? @relation("AgentNodeExecutionOutput", fields: [referencedByOutputExecId], references: [id], onDelete: Cascade)

  agentPresetId String?
  AgentPreset   AgentPreset? @relation("AgentPresetsInputData", fields: [agentPresetId], references: [id])

  // Input and Output pin names are unique for each AgentNodeExecution.
  @@unique([referencedByInputExecId, referencedByOutputExecId, name])
  @@index([referencedByOutputExecId])
  // Composite index for `upsert_execution_input`.
  @@index([name, time])
  @@index([agentPresetId])
}

model AgentNodeExecutionKeyValueData {
  userId               String
  key                  String
  agentNodeExecutionId String
  data                 Json?
  createdAt            DateTime  @default(now())
  updatedAt            DateTime? @updatedAt

  @@id([userId, key])
}

enum ReviewStatus {
  WAITING
  APPROVED
  REJECTED
}

// Pending human reviews for Human-in-the-loop blocks
// Also stores auto-approval records with special nodeExecId patterns (e.g., "auto_approve_{graph_exec_id}_{node_id}")
model PendingHumanReview {
  nodeExecId    String       @id
  userId        String
  graphExecId   String
  graphId       String
  graphVersion  Int
  payload       Json // The actual payload data to be reviewed
  instructions  String? // Instructions/message for the reviewer
  editable      Boolean      @default(true) // Whether the reviewer can edit the data
  status        ReviewStatus @default(WAITING)
  reviewMessage String? // Optional message from the reviewer
  wasEdited     Boolean? // Whether the data was modified during review
  processed     Boolean      @default(false) // Whether the review result has been processed by the execution engine
  createdAt     DateTime     @default(now())
  updatedAt     DateTime?    @updatedAt
  reviewedAt    DateTime?

  User User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([nodeExecId]) // One pending review per node execution
  @@index([userId, status])
  @@index([graphExecId, status])
}

// Webhook that is registered with a provider and propagates to one or more nodes
model IntegrationWebhook {
  id        String    @id @default(uuid())
  createdAt DateTime  @default(now())
  updatedAt DateTime? @updatedAt

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Restrict) // Webhooks must be deregistered before deleting

  provider      String // e.g. 'github'
  credentialsId String // relation to the credentials that the webhook was created with
  webhookType   String // e.g. 'repo'
  resource      String // e.g. 'Significant-Gravitas/AutoGPT'
  events        String[] // e.g. ['created', 'updated']
  config        Json
  secret        String // crypto string, used to verify payload authenticity

  providerWebhookId String // Webhook ID assigned by the provider

  AgentNodes   AgentNode[]
  AgentPresets AgentPreset[]
}

model AnalyticsDetails {
  // PK uses gen_random_uuid() to allow the db inserts to happen outside of prisma
  // typical uuid() inserts are handled by prisma
  id String @id @default(dbgenerated("gen_random_uuid()"))

  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // Link to User model
  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  // Analytics Categorical data used for filtering (indexable w and w/o userId)
  type String

  // Analytic Specific Data. We should use a union type here, but prisma doesn't support it.
  data Json?

  // Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc.
  dataIndex String?

  @@index([userId, type])
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////////   METRICS TRACKING TABLES    ////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model AnalyticsMetrics {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Analytics Categorical data used for filtering (indexable w and w/o userId)
  analyticMetric String
  // Any numeric data that should be counted upon, summed, or otherwise aggregated.
  value          Float
  // Any string data that should be used to identify the metric as distinct.
  // ex: '/build' vs '/market'
  dataString     String?

  // Link to User model
  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////   ACCOUNTING AND CREDIT SYSTEM TABLES    //////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////

enum CreditTransactionType {
  TOP_UP
  USAGE
  GRANT
  REFUND
  CARD_CHECK
  SUBSCRIPTION
}

model CreditTransaction {
  transactionKey String   @default(uuid())
  createdAt      DateTime @default(now())

  userId String
  User   User?  @relation(fields: [userId], references: [id], onDelete: NoAction)

  amount Int
  type   CreditTransactionType

  runningBalance Int?

  isActive Boolean @default(true)
  metadata Json?

  @@id(name: "creditTransactionIdentifier", [transactionKey, userId])
  @@index([userId, createdAt])
}

enum CreditRefundRequestStatus {
  PENDING
  APPROVED
  REJECTED
}

model CreditRefundRequest {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  userId         String
  transactionKey String

  amount Int
  reason String
  result String?
  status CreditRefundRequestStatus @default(PENDING)

  @@index([userId, transactionKey])
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////   Platform Cost Tracking TABLES    //////////////
////////////////////////////////////////////////////////////

model PlatformCostLog {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())

  userId       String?
  User         User?   @relation(fields: [userId], references: [id], onDelete: SetNull)
  graphExecId  String?
  nodeExecId   String?
  graphId      String?
  nodeId       String?
  blockId      String?
  blockName    String?
  provider     String
  credentialId String?

  // Cost in microdollars (1 USD = 1,000,000). Null if unknown.
  costMicrodollars BigInt?

  inputTokens         Int?
  outputTokens        Int?
  cacheReadTokens     Int? // Anthropic cache read tokens (billed at 10% of base)
  cacheCreationTokens Int? // Anthropic cache write tokens (billed at 125% of base)
  dataSize            Int? // bytes
  duration            Float? // seconds
  model               String?
  trackingType        String? // e.g. "cost_usd", "tokens", "characters", "items", "per_run", "sandbox_seconds", "walltime_seconds"
  trackingAmount      Float? // Amount in the unit implied by trackingType
  metadata            Json?

  @@index([userId, createdAt])
  @@index([provider, createdAt])
  @@index([createdAt])
  @@index([graphExecId])
  @@index([provider, trackingType])
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////////   Store TABLES    ///////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////

model SearchTerms {
  // User ID not being logged as this is anonymous analytics data
  // Not using uuid as we want to minimise table size
  id          BigInt   @id @default(autoincrement())
  createdDate DateTime
  searchTerm  String

  @@index([createdDate])
}

model Profile {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  userId String @unique
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  name        String
  username    String @unique
  description String

  links String[]

  avatarUrl String?

  isFeatured Boolean @default(false)

  LibraryAgents LibraryAgent[]
  StoreListings StoreListing[]

  @@index([userId])
}

view Creator {
  username    String @unique
  name        String
  avatar_url  String
  description String

  top_categories String[]
  links          String[]

  num_agents   Int
  agent_rating Float
  agent_runs   Int
  is_featured  Boolean

  // Materialized views used (refreshed every 15 minutes via pg_cron):
  // - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId
  //   * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup
  // - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId
  //   * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup
  //   * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance
  //   * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance
  //
  // Query strategy: Uses CTEs to efficiently aggregate creator statistics leveraging materialized views
}

view StoreAgent {
  listing_id         String   @id
  listing_version_id String
  updated_at         DateTime

  slug              String
  agent_name        String
  agent_video       String?
  agent_output_demo String?
  agent_image       String[]

  featured           Boolean  @default(false)
  creator_username   String?
  creator_avatar     String?
  sub_heading        String
  description        String
  categories         String[]
  runs               Int
  rating             Float
  versions           String[]
  graph_id           String
  graph_versions     String[]
  is_available       Boolean  @default(true)
  use_for_onboarding Boolean  @default(false)

  recommended_schedule_cron String?

  // Materialized views used (refreshed every 15 minutes via pg_cron):
  // - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId
  //   * idx_mv_agent_run_counts (UNIQUE on agentGraphId) - Primary lookup
  // - mv_review_stats - Pre-aggregated review statistics (count, avg rating) by storeListingId
  //   * idx_mv_review_stats (UNIQUE on storeListingId) - Primary lookup
  //   * idx_mv_review_stats_rating (avg_rating DESC) - Sort by rating performance
  //   * idx_mv_review_stats_count (review_count DESC) - Sort by review count performance
  //
  // Query strategy: Uses CTE for version aggregation and joins with materialized views for performance
}

view StoreSubmission {
  // From StoreListing:
  listing_id String
  user_id    String
  slug       String

  // From StoreListingVersion:
  listing_version_id String @id
  listing_version    Int
  graph_id           String
  graph_version      Int

  name                  String
  sub_heading           String
  description           String
  instructions          String?
  categories            String[]
  image_urls            String[]
  video_url             String?
  agent_output_demo_url String?

  submitted_at      DateTime?
  changes_summary   String?
  status            SubmissionStatus
  reviewed_at       DateTime?
  reviewer_id       String?
  review_comments   String?
  internal_comments String?

  is_deleted Boolean

  // Aggregated from AgentGraphExecutions and StoreListingReviews:
  run_count         Int
  review_count      Int
  review_avg_rating Float
}

// Note: This is actually a MATERIALIZED VIEW in the database
// Refreshed automatically every 15 minutes via pg_cron (with fallback to manual refresh)
view mv_agent_run_counts {
  graph_id  String @unique
  run_count Int // excluding runs by the graph's creator

  // Pre-aggregated count of AgentGraphExecution records by agentGraphId.
  // Used by StoreAgent, Creator, and StoreSubmission views for performance optimization.
  // - Should have a unique index on graph_id for fast lookups
  // - Refresh should use CONCURRENTLY to avoid blocking reads
}

// Note: This is actually a MATERIALIZED VIEW in the database
// Refreshed automatically every 15 minutes via pg_cron (with fallback to manual refresh)
view mv_review_stats {
  storeListingId String @unique
  review_count   Int
  avg_rating     Float

  // Pre-aggregated review statistics from StoreListingReview
  // Includes count of reviews and average rating per StoreListing
  // Only includes approved versions (submissionStatus = 'APPROVED') and non-deleted listings
  // Used by StoreAgent view for performance optimization
  // Unique index created automatically on storeListingId for fast lookups
  // Refresh uses CONCURRENTLY to avoid blocking reads
}

// Note: This is actually a MATERIALIZED VIEW in the database
// Refreshed automatically every hour via pg_cron (with fallback to manual refresh)
view mv_suggested_blocks {
  block_id        String @unique
  execution_count Int

  // Pre-aggregated execution counts per block for the last 14 days
  // Used by builder suggestions for ordering blocks by popularity
  // Refresh uses CONCURRENTLY to avoid blocking reads
}

model StoreListing {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  isDeleted          Boolean @default(false)
  // Whether any version has been approved and is available for display
  hasApprovedVersion Boolean @default(false)

  // URL-friendly identifier for this agent (moved from StoreListingVersion)
  slug String

  // Allow this agent to be used during onboarding
  useForOnboarding Boolean @default(false)

  // The currently active version that should be shown to users
  activeVersionId String?              @unique
  ActiveVersion   StoreListingVersion? @relation("ActiveVersion", fields: [activeVersionId], references: [id])

  // The agent link here is only so we can do lookup on agentId
  agentGraphId String @unique

  owningUserId   String
  OwningUser     User    @relation(fields: [owningUserId], references: [id])
  CreatorProfile Profile @relation(fields: [owningUserId], references: [userId], map: "StoreListing_owner_Profile_fkey", onDelete: Cascade)

  // Relations
  Versions StoreListingVersion[] @relation("ListingVersions")

  @@unique([owningUserId, slug])
  // Used in the view query
  @@index([isDeleted, hasApprovedVersion])
}

model StoreListingVersion {
  id        String   @id @default(uuid())
  version   Int      @default(1)
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // The agent and version to be listed on the store
  agentGraphId      String
  agentGraphVersion Int
  AgentGraph        AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version])

  // Content fields
  name               String
  subHeading         String
  videoUrl           String?
  agentOutputDemoUrl String?
  imageUrls          String[]
  description        String
  instructions       String?
  categories         String[]

  isFeatured Boolean @default(false)

  isDeleted   Boolean @default(false)
  // Old versions can be made unavailable by the author if desired
  isAvailable Boolean @default(true)

  // Note: search column removed - now using UnifiedContentEmbedding.search

  // Version workflow state
  submissionStatus SubmissionStatus @default(DRAFT)
  submittedAt      DateTime?

  // Relations
  storeListingId String
  StoreListing   StoreListing @relation("ListingVersions", fields: [storeListingId], references: [id], onDelete: Cascade)

  // This version might be the active version for a listing
  ActiveFor StoreListing? @relation("ActiveVersion")

  // Submission history
  changesSummary String?

  // Review information
  reviewerId       String?
  Reviewer         User?     @relation(fields: [reviewerId], references: [id])
  internalComments String? // Private notes for admin use only
  reviewComments   String? // Comments visible to creator
  reviewedAt       DateTime?

  recommendedScheduleCron String? // cron expression like "0 9 * * *"

  // Reviews for this specific version
  Reviews StoreListingReview[]

  // Note: Embeddings now stored in UnifiedContentEmbedding table
  // Use contentType=STORE_AGENT and contentId=storeListingVersionId

  @@unique([storeListingId, version])
  @@index([storeListingId, submissionStatus, isAvailable])
  @@index([submissionStatus])
  @@index([reviewerId])
  @@index([agentGraphId, agentGraphVersion]) // Non-unique index for efficient lookups
}

// Content type enum for unified search across store agents, blocks, docs
// Note: BLOCK/INTEGRATION are file-based (Python classes), not DB records
// DOCUMENTATION are file-based (.md files), not DB records  
// Only STORE_AGENT and LIBRARY_AGENT are stored in database
enum ContentType {
  STORE_AGENT // Database: StoreListingVersion
  BLOCK // File-based: Python classes in /backend/blocks/
  INTEGRATION // File-based: Python classes (blocks with credentials)
  DOCUMENTATION // File-based: .md/.mdx files
  LIBRARY_AGENT // Database: User's personal agents
}

// Unified embeddings table for all searchable content types
// Supports both public content (userId=null) and user-specific content (userId=userID)
model UnifiedContentEmbedding {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Content identification
  contentType ContentType
  contentId   String // DB ID (storeListingVersionId) or file identifier (block.id, file_path)
  // NULL for public content (store, blocks, docs); userId for private content (library agents).
  // A SQL CHECK constraint enforces ``contentType != 'LIBRARY_AGENT' OR userId IS NOT NULL``
  // (see migration ``20260525120000_library_agent_userid_check``).
  userId      String?

  // Search data
  embedding      Unsupported("vector(1536)") // pgvector embedding (extension in platform schema)
  searchableText String // Combined text for search and fallback
  search         Unsupported("tsvector")?    @default(dbgenerated("''::tsvector")) // Full-text search (auto-populated by trigger)
  metadata       Json                        @default("{}") // Content-specific metadata
  // NO @@index for search - GIN index "UnifiedContentEmbedding_search_idx" created via SQL migration
  // Prisma may generate DROP INDEX on migrate dev - that's okay, migration recreates it

  @@unique([contentType, contentId, userId], map: "UnifiedContentEmbedding_contentType_contentId_userId_key")
  @@index([contentType])
  @@index([userId])
  @@index([contentType, userId])
  @@index([embedding], map: "UnifiedContentEmbedding_embedding_idx")
}

model StoreListingReview {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  storeListingVersionId String
  StoreListingVersion   StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)

  reviewByUserId String
  ReviewByUser   User   @relation(fields: [reviewByUserId], references: [id])

  score    Int
  comments String?

  // Enforce one review per user per listing version
  @@unique([storeListingVersionId, reviewByUserId])
  @@index([storeListingVersionId])
}

enum SubmissionStatus {
  DRAFT // Being prepared, not yet submitted
  PENDING // Submitted, awaiting review
  APPROVED // Reviewed and approved
  REJECTED // Reviewed and rejected
}

enum APIKeyPermission {
  IDENTITY // Info about the authenticated user
  EXECUTE_GRAPH // Can execute agent graphs
  READ_GRAPH // Can get graph versions and details
  WRITE_GRAPH // Can create and update agent graphs
  EXECUTE_BLOCK // Can execute individual blocks
  READ_BLOCK // Can get block information
  READ_STORE // Can read store agents and creators
  WRITE_LIBRARY // Can add agents to library
  USE_TOOLS // Can use chat tools via external API
  MANAGE_INTEGRATIONS // Can initiate OAuth flows and complete them
  READ_INTEGRATIONS // Can list credentials and providers
  DELETE_INTEGRATIONS // Can delete credentials
}

model APIKey {
  id   String  @id @default(uuid())
  name String
  head String // First few chars for identification
  tail String
  hash String  @unique
  salt String? // null for legacy unsalted keys

  status      APIKeyStatus       @default(ACTIVE)
  permissions APIKeyPermission[]

  createdAt  DateTime  @default(now())
  lastUsedAt DateTime?
  revokedAt  DateTime?

  description String?

  // Relation to user
  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([head, name])
  @@index([userId, status])
}

model UserBalance {
  userId    String   @id
  balance   Int      @default(0)
  updatedAt DateTime @updatedAt

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
}

enum APIKeyStatus {
  ACTIVE
  REVOKED
  SUSPENDED
}

////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////////   OAUTH PROVIDER TABLES    //////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////

// OAuth2 applications that can access AutoGPT on behalf of users
model OAuthApplication {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Application metadata
  name             String
  description      String?
  logoUrl          String? // URL to app logo stored in GCS
  clientId         String  @unique
  clientSecret     String // Hashed with Scrypt (same as API keys)
  clientSecretSalt String // Salt for Scrypt hashing

  // OAuth configuration
  redirectUris String[] // Allowed callback URLs
  grantTypes   String[]           @default(["authorization_code", "refresh_token"])
  scopes       APIKeyPermission[] // Which permissions the app can request

  // Application management
  ownerId  String
  Owner    User    @relation(fields: [ownerId], references: [id], onDelete: Cascade)
  isActive Boolean @default(true)

  // Relations
  AuthorizationCodes OAuthAuthorizationCode[]
  AccessTokens       OAuthAccessToken[]
  RefreshTokens      OAuthRefreshToken[]

  @@index([clientId])
  @@index([ownerId])
}

// Temporary authorization codes (10 min TTL)
model OAuthAuthorizationCode {
  id        String   @id @default(uuid())
  code      String   @unique
  createdAt DateTime @default(now())
  expiresAt DateTime // Now + 10 minutes

  applicationId String
  Application   OAuthApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade)

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  scopes      APIKeyPermission[]
  redirectUri String // Must match one from application

  // PKCE (Proof Key for Code Exchange) support
  codeChallenge       String?
  codeChallengeMethod String? // "S256" or "plain"

  usedAt DateTime? // Set when code is consumed

  @@index([code])
  @@index([applicationId, userId])
  @@index([expiresAt]) // For cleanup
}

// Access tokens (1 hour TTL)
model OAuthAccessToken {
  id        String   @id @default(uuid())
  token     String   @unique // SHA256 hash of plaintext token
  createdAt DateTime @default(now())
  expiresAt DateTime // Now + 1 hour

  applicationId String
  Application   OAuthApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade)

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  scopes APIKeyPermission[]

  revokedAt DateTime? // Set when token is revoked

  @@index([token]) // For token lookup
  @@index([userId, applicationId])
  @@index([expiresAt]) // For cleanup
}

// Refresh tokens (30 days TTL)
model OAuthRefreshToken {
  id        String   @id @default(uuid())
  token     String   @unique // SHA256 hash of plaintext token
  createdAt DateTime @default(now())
  expiresAt DateTime // Now + 30 days

  applicationId String
  Application   OAuthApplication @relation(fields: [applicationId], references: [id], onDelete: Cascade)

  userId String
  User   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  scopes APIKeyPermission[]

  revokedAt DateTime? // Set when token is revoked

  @@index([token]) // For token lookup
  @@index([userId, applicationId])
  @@index([expiresAt]) // For cleanup
}

// ── Platform Bot Linking ──────────────────────────────────────────────
// Links external chat platform identities (Discord, Telegram, Slack, etc.)
// to AutoGPT user accounts, enabling the multi-platform CoPilot bot.

enum PlatformType {
  DISCORD
  TELEGRAM
  SLACK
  TEAMS
  WHATSAPP
  GITHUB
  LINEAR
}

// Whether a linking token claims a server (group chat / guild) or a personal
// 1:1 user link (DMs). Server and user links are completely independent —
// linking a server does not grant DM access and vice versa.
enum PlatformLinkType {
  SERVER
  USER
}

// Maps a platform server (Discord guild, Telegram group, Slack workspace, etc.)
// to an AutoGPT user account. The user who first authenticates becomes the
// "owner" — all usage from that server is attributed to their account.
model PlatformLink {
  id                  String       @id @default(uuid())
  userId              String // AutoGPT user ID of the owner
  User                User         @relation(fields: [userId], references: [id], onDelete: Cascade)
  platform            PlatformType
  platformServerId    String // Server/guild/group ID on that platform
  ownerPlatformUserId String // Platform user ID of the person who set it up
  serverName          String? // Display name of the server (best-effort, may go stale)
  linkedAt            DateTime     @default(now())

  @@unique([platform, platformServerId])
  @@index([userId])
}

// Maps a platform user identity (a single Discord / Telegram / Slack user) to
// an AutoGPT account for 1:1 DM conversations with the bot. Independent from
// PlatformLink — a user who owns a linked server must still link their DMs
// separately.
model PlatformUserLink {
  id               String       @id @default(uuid())
  userId           String // AutoGPT user ID
  User             User         @relation(fields: [userId], references: [id], onDelete: Cascade)
  platform         PlatformType
  platformUserId   String // Individual's user ID on the platform
  platformUsername String? // Display name at link time (best-effort)
  linkedAt         DateTime     @default(now())

  @@unique([platform, platformUserId])
  @@index([userId])
}

// One-time tokens for either the server linking flow or the DM (user) linking
// flow. linkType determines which target is populated — SERVER tokens carry
// platformServerId + serverName + ownerPlatformUserId, USER tokens carry
// platformUserId only.
model PlatformLinkToken {
  id               String           @id @default(uuid())
  token            String           @unique
  platform         PlatformType
  linkType         PlatformLinkType @default(SERVER)
  // SERVER token fields (null for USER tokens)
  platformServerId String? // Server/guild/group ID being linked
  serverName       String? // Server display name
  channelId        String? // Channel to send confirmation back to
  // Always set — platform user ID of the person who will claim ownership
  platformUserId   String
  platformUsername String? // Their display name
  expiresAt        DateTime
  usedAt           DateTime?
  createdAt        DateTime         @default(now())

  @@index([platform, platformServerId])
  @@index([platform, platformUserId])
  @@index([expiresAt])
}
