generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

// ─── Enums ───────────────────────────────────────────────

enum WorkspaceProvider {
  GITHUB
  GITLAB
  BITBUCKET
  SLACK
  JIRA
  CONFLUENCE
  FIGMA
  CLICKUP
  GOOGLE_DRIVE
  GMAIL
  MICROSOFT_SHAREPOINT
  MICROSOFT_ONEDRIVE
  GOOGLE_CALENDAR
  OUTLOOK_CALENDAR
}

enum WorkspaceProviderAuthMode {
  OAUTH2
  PAT
  BASIC
  SERVICE_ACCOUNT
  NONE
}

enum WorkspaceProviderDefinitionStatus {
  ACTIVE
  BETA
  DEPRECATED
  DISABLED
}

enum WorkspaceProviderAppConfigStatus {
  DRAFT
  READY
  DISABLED
  INVALID
}

enum WorkspaceConnectorStatus {
  CONNECTED
  DEGRADED
  DISCONNECTED
  PENDING_AUTH
  PAUSED
  UNKNOWN
}

enum WorkspaceSyncStatus {
  RUNNING
  COMPLETED
  FAILED
  PARTIAL
}

enum WorkspaceObjectType {
  REPOSITORY
  ISSUE
  PULL_REQUEST
  MESSAGE
  CHANNEL
  DOCUMENT
  FILE
  TICKET
  COMMENT
  USER
  PROJECT
  EMAIL
  MEETING_NOTE
  SPREADSHEET
  MEETING
}

enum WorkspacePermissionLevel {
  READ
  WRITE
  ADMIN
}

enum WorkspaceActionType {
  CREATE_ISSUE
  CREATE_ISSUE_COMMENT
  CREATE_PR_DESCRIPTION
  CREATE_TICKET
  ADD_TICKET_COMMENT
  SEND_SLACK_MESSAGE
  SEND_EMAIL
  REPLY_EMAIL
  CREATE_DRAFT
  UPDATE_JIRA_ISSUE
  COMMENT_JIRA
  COMMENT_PR
  APPROVE_PR
  ADD_PR_SUGGESTION
  SEND_SLACK
  REPLY_SLACK
  EDIT_CONFLUENCE
  CREATE_CONFLUENCE
  UPLOAD_DRIVE
  MOVE_DRIVE
  CREATE_USER_STORY_FROM_FIGMA
  CREATE_JIRA_FROM_FIGMA
  POST_FIGMA_COMMENT
  // Stream 20 — GitLab/Bitbucket write actions
  CREATE_MR_COMMENT
  APPROVE_MR
  CREATE_GITLAB_ISSUE
  COMMENT_GITLAB_ISSUE
  UPDATE_MR_DESCRIPTION
  CREATE_PR_COMMENT_BB
  APPROVE_PR_BB
  CREATE_BITBUCKET_ISSUE
  // v3 round 2 (2026-05-12) — line-level GitLab MR discussion
  ADD_MR_SUGGESTION
  // v3 round 6 (2026-05-12) — image-anchored GitLab MR comment
  ADD_MR_IMAGE_COMMENT
  // Stream 21 — OneDrive/SharePoint/ClickUp write actions
  UPLOAD_ONEDRIVE
  MOVE_ONEDRIVE
  UPLOAD_SHAREPOINT
  CREATE_SHAREPOINT_LIST_ITEM
  UPDATE_SHAREPOINT_LIST_ITEM
  CREATE_CLICKUP_TASK
  UPDATE_CLICKUP_TASK
  COMMENT_CLICKUP_TASK
}

enum WorkspaceActionStatus {
  PENDING_APPROVAL
  APPROVED
  REJECTED
  EXECUTING
  EXECUTED
  FAILED
  EXPIRED
  EDITED
}

// ─── Stream 10: AI Action Approval Policy Engine ─────────

enum AiActionPolicyKind {
  DENY
  ALLOW
  AUTO_APPROVE
}

enum AiActionRiskLabel {
  LOW
  MEDIUM
  HIGH
  CRITICAL
}

enum AiActionQueueStatus {
  PENDING_APPROVAL
  AUTO_APPROVED
  APPROVED
  REJECTED
  EXECUTING
  EXECUTED
  FAILED
  EXPIRED
  EDITED
  DENIED
}

// ─── Models ──────────────────────────────────────────────

model WorkspaceConnector {
  id                  String                    @id @default(cuid())
  userId              String                    @map("user_id")
  name                String
  provider            WorkspaceProvider
  providerAppConfigId String?                   @map("provider_app_config_id")
  authMode            WorkspaceProviderAuthMode? @map("auth_mode")
  status              WorkspaceConnectorStatus  @default(PENDING_AUTH)
  statusReason        String?                   @map("status_reason")
  permissionLevel     WorkspacePermissionLevel  @default(READ) @map("permission_level")
  encryptedTokens     String?                   @map("encrypted_tokens")
  tokenVersion        Int                       @default(0) @map("token_version")
  scopes              String[]                  @default([])
  expiresAt           DateTime?                 @map("expires_at")
  lastSyncAt          DateTime?                 @map("last_sync_at")
  lastAuthAt          DateTime?                 @map("last_auth_at")
  deltaToken          String?                   @map("delta_token")
  isEnabled           Boolean                   @default(true) @map("is_enabled")
  webhookId           String?                   @map("webhook_id")
  metadata            Json?
  createdAt           DateTime                  @default(now()) @map("created_at")
  updatedAt           DateTime                  @updatedAt @map("updated_at")

  // Stream 01 Phase 5 — scheduled sync cadence + state
  syncIntervalSeconds Int?                      @map("sync_interval_seconds")
  backfillWindowDays  Int?                      @map("backfill_window_days")
  pausedAt            DateTime?                 @map("paused_at")
  pauseReason         String?                   @map("pause_reason")
  rateLimitState      Json?                     @map("rate_limit_state")
  circuitState        Json?                     @map("circuit_state")
  lastTickAt          DateTime?                 @map("last_tick_at")
  lastRunKey          String?                   @map("last_run_key")

  objectCount   Int                    @default(0) @map("object_count")

  appConfig     WorkspaceProviderAppConfig? @relation(fields: [providerAppConfigId], references: [id], onDelete: SetNull)
  syncRuns      WorkspaceSyncRun[]
  healthEvents  WorkspaceHealthEvent[]
  objects       WorkspaceObject[]
  actions       WorkspaceAction[]

  @@index([userId])
  @@index([provider])
  @@index([status])
  @@index([isEnabled])
  @@index([providerAppConfigId])
  @@map("workspace_connectors")
}

model WorkspaceSyncRun {
  id              String              @id @default(cuid())
  connectorId     String              @map("connector_id")
  status          WorkspaceSyncStatus
  objectType      WorkspaceObjectType @map("object_type")
  isDelta         Boolean             @default(false) @map("is_delta")
  objectsFound    Int                 @default(0) @map("objects_found")
  objectsSynced   Int                 @default(0) @map("objects_synced")
  objectsFailed   Int                 @default(0) @map("objects_failed")
  deltaTokenIn    String?             @map("delta_token_in")
  deltaTokenOut   String?             @map("delta_token_out")
  startedAt       DateTime            @default(now()) @map("started_at")
  completedAt     DateTime?           @map("completed_at")
  errorMessage    String?             @map("error_message")

  // Stream 01 Phase 5 — run observability
  durationMs      Int?                @map("duration_ms")
  retryCount      Int                 @default(0) @map("retry_count")
  cursorBefore    String?             @map("cursor_before")
  cursorAfter     String?             @map("cursor_after")
  errorCode       String?             @map("error_code")
  isDryRun        Boolean             @default(false) @map("is_dry_run")
  triggeredBy     String              @default("manual") @map("triggered_by")
  runKey          String?             @map("run_key")

  connector WorkspaceConnector @relation(fields: [connectorId], references: [id], onDelete: Cascade)

  @@index([connectorId])
  @@index([status])
  @@index([startedAt])
  @@index([connectorId, startedAt(sort: Desc)], map: "workspace_sync_runs_connector_started_idx")
  @@unique([connectorId, runKey], map: "workspace_sync_runs_connector_run_key_key")
  @@map("workspace_sync_runs")
}

model SyncCadenceDefault {
  provider             WorkspaceProvider @id
  intervalSeconds      Int               @map("interval_seconds")
  backfillWindowDays   Int               @default(30) @map("backfill_window_days")
  priority             Int               @default(50)
  supportsDeltaSync    Boolean           @default(false) @map("supports_delta_sync")
  supportsWebhookSync  Boolean           @default(false) @map("supports_webhook_sync")
  nativeCursorKind     String?           @map("native_cursor_kind")
  notes                String?
  updatedAt            DateTime          @default(now()) @updatedAt @map("updated_at")

  @@map("workspace_sync_cadence_defaults")
}

model WorkspaceHealthEvent {
  id           String                   @id @default(cuid())
  connectorId  String                   @map("connector_id")
  status       WorkspaceConnectorStatus
  latencyMs    Int?                     @map("latency_ms")
  errorMessage String?                  @map("error_message")
  checkedAt    DateTime                 @default(now()) @map("checked_at")

  connector WorkspaceConnector @relation(fields: [connectorId], references: [id], onDelete: Cascade)

  @@index([connectorId])
  @@index([checkedAt])
  @@map("workspace_health_events")
}

model WorkspaceObject {
  id                String              @id @default(cuid())
  connectorId       String              @map("connector_id")
  userId            String              @map("user_id")
  externalId        String              @map("external_id")
  type              WorkspaceObjectType
  title             String
  content           String?
  url               String?
  authorId          String?             @map("author_id")
  provider          WorkspaceProvider
  metadata          Json                @default("{}")
  externalCreatedAt DateTime?           @map("external_created_at")
  externalUpdatedAt DateTime?           @map("external_updated_at")
  createdAt         DateTime            @default(now()) @map("created_at")
  updatedAt         DateTime            @updatedAt @map("updated_at")

  connector    WorkspaceConnector    @relation(fields: [connectorId], references: [id], onDelete: Cascade)
  sourceLinks  WorkspaceObjectLink[] @relation("SourceObject")
  targetLinks  WorkspaceObjectLink[] @relation("TargetObject")

  @@unique([connectorId, externalId])
  @@index([userId])
  @@index([connectorId])
  @@index([type])
  @@map("workspace_objects")
}

model WorkspaceAction {
  id              String                @id @default(cuid())
  userId          String                @map("user_id") @db.VarChar(128)
  connectorId     String                @map("connector_id")
  actionType      WorkspaceActionType   @map("action_type")
  status          WorkspaceActionStatus @default(PENDING_APPROVAL) @map("status")
  payload         Json
  result          Json?
  idempotencyKey  String?               @map("idempotency_key") @db.VarChar(128)
  retryCount      Int                   @default(0) @map("retry_count")
  requestedAt     DateTime              @default(now()) @map("requested_at")
  reviewedAt      DateTime?             @map("reviewed_at")
  executedAt      DateTime?             @map("executed_at")
  expiresAt       DateTime?             @map("expires_at")
  reviewedBy      String?               @map("reviewed_by") @db.VarChar(128)
  rejectionReason String?               @map("rejection_reason") @db.VarChar(1000)
  errorMessage    String?               @map("error_message") @db.VarChar(2000)

  // Stream 02 Phase 5 — v3 approval center
  generatedBy     Json?                 @map("generated_by")
  draftHistory    Json                  @default("[]") @map("draft_history")
  sourceObjectId  String?               @map("source_object_id")
  sourceFetchedAt DateTime?             @map("source_fetched_at")
  bulkGroupId     String?               @map("bulk_group_id")

  connector WorkspaceConnector @relation(fields: [connectorId], references: [id], onDelete: Cascade)

  @@unique([userId, idempotencyKey])
  @@index([userId])
  @@index([connectorId])
  @@index([status])
  @@index([requestedAt])
  @@index([bulkGroupId], map: "workspace_actions_bulk_group_idx")
  @@map("workspace_actions")
}

model WorkspaceProviderDefinition {
  id               String                            @id @default(cuid())
  provider         WorkspaceProvider                 @unique
  displayName      String                            @map("display_name")
  description      String?
  authModes        WorkspaceProviderAuthMode[]       @map("auth_modes")
  defaultAuthMode  WorkspaceProviderAuthMode         @map("default_auth_mode")
  configSchema     Json                              @map("config_schema")
  capabilities     Json                              @default("{}")
  supportedObjects WorkspaceObjectType[]             @map("supported_objects")
  supportedActions WorkspaceActionType[]             @map("supported_actions")
  iconUrl          String?                           @map("icon_url")
  docsUrl          String?                           @map("docs_url")
  adapterKey       String                            @map("adapter_key")
  status           WorkspaceProviderDefinitionStatus @default(ACTIVE)
  version          Int                               @default(1)
  createdAt        DateTime                          @default(now()) @map("created_at")
  updatedAt        DateTime                          @updatedAt @map("updated_at")

  appConfigs WorkspaceProviderAppConfig[]

  @@index([status])
  @@map("workspace_provider_definitions")
}

model WorkspaceProviderAppConfig {
  id              String                           @id @default(cuid())
  provider        WorkspaceProvider
  definitionId    String                           @map("definition_id")
  name            String
  description     String?
  authMode        WorkspaceProviderAuthMode        @map("auth_mode")
  publicConfig    Json                             @default("{}") @map("public_config")
  encryptedSecret String?                          @map("encrypted_secret")
  secretVersion   Int                              @default(0) @map("secret_version")
  scopes          String[]                         @default([])
  status          WorkspaceProviderAppConfigStatus @default(DRAFT)
  lastValidatedAt DateTime?                        @map("last_validated_at")
  validationError String?                          @map("validation_error")
  createdBy       String                           @map("created_by")
  createdAt       DateTime                         @default(now()) @map("created_at")
  updatedAt       DateTime                         @updatedAt @map("updated_at")

  definition WorkspaceProviderDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade)
  connectors WorkspaceConnector[]

  @@unique([provider, name])
  @@index([status])
  @@index([definitionId])
  @@map("workspace_provider_app_configs")
}

model WorkspaceObjectLink {
  id             String  @id @default(cuid())
  sourceObjectId String  @map("source_object_id")
  targetObjectId String? @map("target_object_id")
  externalRef    String? @map("external_ref")
  linkType       String  @map("link_type")
  confidence     Float   @default(1.0)

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

  sourceObject WorkspaceObject  @relation("SourceObject", fields: [sourceObjectId], references: [id], onDelete: Cascade)
  targetObject WorkspaceObject? @relation("TargetObject", fields: [targetObjectId], references: [id], onDelete: SetNull)

  @@index([sourceObjectId])
  @@index([targetObjectId])
  @@map("workspace_object_links")
}

// ─── Stream 10: AI Action Approval Policy Engine — Models ─

model AiActionPolicy {
  id                String              @id @default(cuid())
  name              String              @unique
  kind              AiActionPolicyKind
  description       String?
  providerRegex     String              @default(".*") @map("provider_regex")
  actionKindRegex   String              @default(".*") @map("action_kind_regex")
  riskMaxLabel      AiActionRiskLabel   @default(LOW) @map("risk_max_label")
  riskMaxScore      Int                 @default(30) @map("risk_max_score")
  priority          Int                 @default(0)
  requireReason     Boolean             @default(false) @map("require_reason")
  isActive          Boolean             @default(true) @map("is_active")
  isSystemDefault   Boolean             @default(false) @map("is_system_default")
  createdBy         String?             @map("created_by") @db.VarChar(128)
  createdAt         DateTime            @default(now()) @map("created_at")
  updatedAt         DateTime            @updatedAt @map("updated_at")

  queueEntries      AiActionApprovalQueue[]

  @@index([isActive, priority])
  @@index([kind])
  @@map("ai_action_policies")
}

model AiActionApprovalQueue {
  id                String              @id @default(cuid())
  userId            String              @map("user_id") @db.VarChar(128)
  connectorId       String?             @map("connector_id")
  actionKind        String              @map("action_kind") @db.VarChar(64)
  provider          WorkspaceProvider?
  status            AiActionQueueStatus @default(PENDING_APPROVAL)
  draftPayload      Json                @map("draft_payload")
  editedPayload     Json?               @map("edited_payload")
  riskLabel         AiActionRiskLabel   @default(LOW) @map("risk_label")
  riskScore         Int                 @default(0) @map("risk_score")
  riskReasons       Json                @default("[]") @map("risk_reasons")
  matchedPolicyId   String?             @map("matched_policy_id")
  matchedPolicyName String?             @map("matched_policy_name") @db.VarChar(256)
  generatedBy       Json?               @map("generated_by")
  sourceObjectId    String?             @map("source_object_id")
  rejectionReason   String?             @map("rejection_reason") @db.VarChar(1000)
  errorMessage      String?             @map("error_message") @db.VarChar(2000)
  workspaceActionId String?             @unique @map("workspace_action_id")
  expiresAt         DateTime?           @map("expires_at")
  statusChangedAt   DateTime            @default(now()) @map("status_changed_at")
  createdAt         DateTime            @default(now()) @map("created_at")
  updatedAt         DateTime            @updatedAt @map("updated_at")

  matchedPolicy     AiActionPolicy?     @relation(fields: [matchedPolicyId], references: [id], onDelete: SetNull)

  @@index([userId, status])
  @@index([status, expiresAt])
  @@index([connectorId])
  @@index([createdAt])
  @@map("ai_action_approval_queue")
}

// ─── Stream 11: Universal Webhook Receiver ───────────────

model WebhookDelivery {
  id              String              @id @default(cuid())
  connectorId     String?             @map("connector_id")
  provider        WorkspaceProvider
  externalDeliveryId String?          @map("external_delivery_id") @db.VarChar(256)
  eventType       String?             @map("event_type") @db.VarChar(128)
  signatureValid  Boolean             @default(false) @map("signature_valid")
  signature       String?             @db.VarChar(512)
  rawPayload      Json                @map("raw_payload")
  processedAt     DateTime?           @map("processed_at")
  errorMessage    String?             @map("error_message") @db.VarChar(2000)
  ipAddress       String?             @map("ip_address") @db.VarChar(64)
  bodyBytes       Int                 @default(0) @map("body_bytes")
  createdAt       DateTime            @default(now()) @map("created_at")

  @@unique([provider, externalDeliveryId])
  @@index([provider, createdAt])
  @@index([signatureValid, createdAt])
  @@index([connectorId])
  @@map("webhook_deliveries")
}

// ─── Stream 12: Auto-Suggest Scheduler ───────────────────

enum AutoSuggestRunStatus {
  RUNNING
  COMPLETED
  FAILED
}

model AutoSuggestRun {
  id                  String                @id @default(cuid())
  jobType             String                @map("job_type") @db.VarChar(64)
  status              AutoSuggestRunStatus
  startedAt           DateTime              @default(now()) @map("started_at")
  completedAt         DateTime?             @map("completed_at")
  candidateCount      Int                   @default(0) @map("candidate_count")
  suggestionsCreated  Int                   @default(0) @map("suggestions_created")
  durationMs          Int?                  @map("duration_ms")
  errorMessage        String?               @map("error_message") @db.VarChar(2000)

  @@index([jobType, startedAt])
  @@index([status, startedAt])
  @@map("auto_suggest_runs")
}

model SuggestionDeduplication {
  id              String     @id @default(cuid())
  userId          String     @map("user_id") @db.VarChar(128)
  sourceObjectId  String     @map("source_object_id")
  actionKind      String     @map("action_kind") @db.VarChar(64)
  jobType         String?    @map("job_type") @db.VarChar(64)
  suggestedAt     DateTime   @default(now()) @map("suggested_at")
  expiresAt       DateTime   @map("expires_at")

  @@unique([userId, sourceObjectId, actionKind])
  @@index([expiresAt])
  @@map("suggestion_deduplication")
}

// ─── Stream 13: Suggestion Factory ───────────────────────

model SuggestionTriggerRule {
  id                    String              @id @default(cuid())
  name                  String              @unique
  description           String?
  eventType             String              @map("event_type") @db.VarChar(64)
  providerRegex         String              @default(".*") @map("provider_regex")
  contentRegex          String              @default(".*") @map("content_regex")
  actionKindToSuggest   String              @map("action_kind_to_suggest") @db.VarChar(64)
  isActive              Boolean             @default(true) @map("is_active")
  isSystemDefault       Boolean             @default(false) @map("is_system_default")
  priority              Int                 @default(0)
  // Stream 13.3 v1.1 — per-rule cap on the number of times THIS rule may
  // enqueue suggestions per hour. null = unbounded (rely on the global
  // per-eventType limiter). Read by SuggestionFactoryRateLimiterManager
  // in addition to the env-driven global cap.
  perRuleBudgetPerHour  Int?                @map("per_rule_budget_per_hour")
  createdBy             String?             @map("created_by") @db.VarChar(128)
  createdAt             DateTime            @default(now()) @map("created_at")
  updatedAt             DateTime            @updatedAt @map("updated_at")

  @@index([isActive, priority])
  @@index([eventType])
  @@map("suggestion_trigger_rules")
}

// ─── Stream 41: Ticket planning + coding bridge ──────────

enum ImplPromptHandoffMode {
  CHAT
  AGENT
  CLIPBOARD
}

enum ImplPromptHandoffStatus {
  PENDING
  DELIVERED
  FAILED
}

model ImplPromptHandoff {
  id                      String                  @id @default(cuid())
  sourceQueueId           String                  @map("source_queue_id") @db.VarChar(128)
  userId                  String                  @map("user_id") @db.VarChar(128)
  mode                    ImplPromptHandoffMode
  targetThreadId          String?                 @map("target_thread_id") @db.VarChar(128)
  targetTerminalCommandId String?                 @map("target_terminal_command_id") @db.VarChar(128)
  status                  ImplPromptHandoffStatus @default(PENDING)
  errorMessage            String?                 @map("error_message") @db.Text
  briefSnippet            String                  @map("brief_snippet") @db.VarChar(1024)
  createdAt               DateTime                @default(now()) @map("created_at")
  deliveredAt             DateTime?               @map("delivered_at")

  @@index([userId])
  @@index([sourceQueueId])
  @@index([status])
  @@map("impl_prompt_handoffs")
}

// ─── Stream 31: Daily / weekly digest dashboard ─────────

enum DigestScope {
  DAILY
  WEEKLY
}

model DigestSnapshot {
  id                      String      @id @default(cuid())
  userId                  String      @map("user_id") @db.VarChar(128)
  scope                   DigestScope
  snapshotDate            DateTime    @map("snapshot_date") @db.Date
  sections                Json        @default("{}")
  actionItemSuggestionIds Json        @default("[]") @map("action_item_suggestion_ids")
  generatedAt             DateTime    @default(now()) @map("generated_at")
  modelUsed               String      @map("model_used") @db.VarChar(128)
  durationMs              Int         @default(0) @map("duration_ms")
  errorMessage            String?     @map("error_message") @db.Text

  @@unique([userId, scope, snapshotDate])
  @@index([userId, scope])
  @@map("digest_snapshots")
}

model UserDigestPreference {
  id              String   @id @default(cuid())
  userId          String   @unique @map("user_id") @db.VarChar(128)
  dailyEnabled    Boolean  @default(true) @map("daily_enabled")
  weeklyEnabled   Boolean  @default(true) @map("weekly_enabled")
  dailyHourLocal  Int      @default(8) @map("daily_hour_local") @db.SmallInt
  weeklyDayOfWeek Int      @default(5) @map("weekly_day_of_week") @db.SmallInt
  weeklyHourLocal Int      @default(8) @map("weekly_hour_local") @db.SmallInt
  timezone        String   @default("UTC") @db.VarChar(64)
  providers       Json     @default("[]")
  lastDailyAt     DateTime? @map("last_daily_at")
  lastWeeklyAt    DateTime? @map("last_weekly_at")
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")

  @@map("user_digest_preferences")
}

// ─── Stream 32: Per-user automation preferences ──────────

model UserAutomationPreference {
  id                        String   @id @default(cuid())
  userId                    String   @map("user_id") @db.VarChar(128)
  actionKind                String   @map("action_kind") @db.VarChar(64)
  isEnabled                 Boolean  @default(true) @map("is_enabled")
  autoApproveBelowRiskScore Int?     @map("auto_approve_below_risk_score")
  perDayBudget              Int?     @map("per_day_budget")
  providers                 Json     @default("[]") @map("providers")
  createdAt                 DateTime @default(now()) @map("created_at")
  updatedAt                 DateTime @updatedAt @map("updated_at")

  @@unique([userId, actionKind])
  @@index([userId])
  @@map("user_automation_preferences")
}

// v3 round 5 (2026-05-12) — Prompt 06 polish: per-user Gmail signature
// library. Callers reference a row by id when composing a send/draft;
// the Gmail adapter appends the body with the RFC 3676 separator. One
// row per user can be flagged isDefault — picked up automatically when
// the caller doesn't supply a signature id.
model UserEmailSignature {
  id        String   @id @default(cuid())
  userId    String   @map("user_id") @db.VarChar(128)
  name      String   @db.VarChar(120)
  body      String   @db.Text
  isDefault Boolean  @default(false) @map("is_default")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@unique([userId, name])
  @@index([userId])
  @@map("user_email_signatures")
}

// v3 round 9 (2026-05-14) — Prompt 06 polish: per-user email template
// library. Unlike a signature (body-only, appended), a template carries
// both a subject and a body and is the *starting point* of an
// AI-drafted or hand-written email. {{placeholder}} tokens in the body
// are resolved by the caller (or left for the AI action to fill).
model UserEmailTemplate {
  id        String   @id @default(cuid())
  userId    String   @map("user_id") @db.VarChar(128)
  name      String   @db.VarChar(120)
  subject   String   @db.VarChar(500)
  body      String   @db.Text
  isDefault Boolean  @default(false) @map("is_default")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@unique([userId, name])
  @@index([userId])
  @@map("user_email_templates")
}

// v3 round 5 (2026-05-12) — Prompt 12 polish: per-connector RBAC. The
// connector owner (WorkspaceConnector.userId) always has full access.
// Grants below let the owner share a connector with another user at a
// specific scope — e.g. share a Jira connector with a teammate but only
// allow them to queue AI suggestions, not see raw tokens or sync state.
//
// Access is the union of (owner-of-connector) ∪ (grantee-with-row-here).
// Lookups are by (userId, connectorId) — both indexed.
enum WorkspaceConnectorAccessLevel {
  READ_ONLY        // can view object list + run AI actions read-only kinds
  AI_ACTIONS       // READ_ONLY + can submit write-action proposals into the approval queue
  FULL             // AI_ACTIONS + can edit connector config, rotate tokens
}

model WorkspaceConnectorGrant {
  id          String                        @id @default(cuid())
  connectorId String                        @map("connector_id")
  userId      String                        @map("user_id") @db.VarChar(128)
  grantedBy   String                        @map("granted_by") @db.VarChar(128)
  accessLevel WorkspaceConnectorAccessLevel @default(AI_ACTIONS) @map("access_level")
  createdAt   DateTime                      @default(now()) @map("created_at")
  updatedAt   DateTime                      @updatedAt @map("updated_at")

  @@unique([connectorId, userId])
  @@index([userId])
  @@index([connectorId])
  @@map("workspace_connector_grants")
}

// v3 round 12 (2026-05-14) — Prompt 11: cross-workspace automation
// chains. A chain is an ordered list of workspace-action steps —
// e.g. "create Jira ticket" → "post Slack message" referencing the new
// ticket's key. Each step's payload may contain {{steps.N.output.field}}
// placeholders that the executor resolves from prior step outputs.
//
// The executor is SEQUENTIAL (step 1 → 2 → 3); a step can read any
// earlier step's output. A true parallel DAG is a future enhancement —
// sequential covers the Gmail→Slack→Jira use case named in the v3 plan.
enum WorkspaceChainRunStatus {
  PENDING
  RUNNING
  COMPLETED
  FAILED
}

enum WorkspaceChainStepStatus {
  PENDING
  RUNNING
  SUCCEEDED
  FAILED
  SKIPPED
}

model WorkspaceChain {
  id          String   @id @default(cuid())
  userId      String   @map("user_id") @db.VarChar(128)
  name        String   @db.VarChar(160)
  description String?  @db.Text
  // The chain DSL: { steps: [{ id, connectorId, actionType, payload }] }.
  // Validated by a Zod schema at the controller layer.
  dsl         Json
  isEnabled   Boolean  @default(true) @map("is_enabled")
  version     Int      @default(1)
  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")

  runs WorkspaceChainRun[]

  @@unique([userId, name])
  @@index([userId])
  @@map("workspace_chains")
}

model WorkspaceChainRun {
  id        String                  @id @default(cuid())
  chainId   String                  @map("chain_id")
  userId    String                  @map("user_id") @db.VarChar(128)
  status    WorkspaceChainRunStatus @default(PENDING)
  // Snapshot of the chain DSL at run time so edits to the chain don't
  // retroactively change a historical run.
  dslSnapshot Json                  @map("dsl_snapshot")
  error     String?                 @db.Text
  startedAt DateTime?               @map("started_at")
  finishedAt DateTime?              @map("finished_at")
  createdAt DateTime                @default(now()) @map("created_at")
  updatedAt DateTime                @updatedAt @map("updated_at")

  chain WorkspaceChain          @relation(fields: [chainId], references: [id], onDelete: Cascade)
  steps WorkspaceChainRunStep[]

  @@index([chainId])
  @@index([userId])
  @@map("workspace_chain_runs")
}

model WorkspaceChainRunStep {
  id          String                   @id @default(cuid())
  runId       String                   @map("run_id")
  // The step id from the DSL — placeholders reference this.
  stepId      String                   @map("step_id") @db.VarChar(120)
  stepIndex   Int                      @map("step_index")
  connectorId String                   @map("connector_id")
  actionType  String                   @map("action_type") @db.VarChar(64)
  status      WorkspaceChainStepStatus @default(PENDING)
  // Resolved payload (placeholders substituted) actually sent to the
  // adapter, plus the adapter's WriteActionResult output.
  resolvedPayload Json?                @map("resolved_payload")
  output      Json?
  error       String?                  @db.Text
  startedAt   DateTime?                @map("started_at")
  finishedAt  DateTime?                @map("finished_at")
  createdAt   DateTime                 @default(now()) @map("created_at")

  run WorkspaceChainRun @relation(fields: [runId], references: [id], onDelete: Cascade)

  @@index([runId])
  @@map("workspace_chain_run_steps")
}
