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

datasource db {
  provider = "postgresql"
}

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

enum SearchProviderKind {
  OLLAMA_WEB
  TAVILY
  SEARXNG
  EXA
  FIRECRAWL
  BRAVE
  SERPAPI
  GENERIC_HTTP
}

enum SearchProviderStatus {
  ACTIVE
  DISABLED
  INVALID
}

enum SearchRunStatus {
  RUNNING
  COMPLETED
  FAILED
}

enum ResearchWorkflowKind {
  SEARCH_ONLY
  SEARCH_THEN_FETCH
  SEARCH_FETCH_EXTRACT
  REPO_CLONE_ANALYZE
}

enum FetchJobStatus {
  RUNNING
  COMPLETED
  FAILED
  CACHED
}

enum ResearchRunStatus {
  RUNNING
  COMPLETED
  FAILED
}

// ─── Tables ──────────────────────────────────────────────

/// A DB-backed search provider definition. Admin-managed.
/// Credentials live in `encryptedSecret` (AES-256-GCM) — never in .env.
model SearchProvider {
  id              String               @id @default(cuid())
  kind            SearchProviderKind
  name            String               @unique
  description     String?
  /// Base URL for the provider (e.g. https://api.tavily.com, SearXNG host, or Ollama host).
  baseUrl         String
  /// Encrypted secret blob — typically {"apiKey": "..."} JSON.
  encryptedSecret String?
  secretVersion   Int                  @default(0)
  enabled         Boolean              @default(true)
  priority        Int                  @default(100)
  status          SearchProviderStatus @default(ACTIVE)
  /// Additional provider-specific non-secret config (e.g. {"maxResults":10}).
  publicConfig    Json                 @default("{}")
  /// Per-provider domain allow/block lists.
  allowlistDomains String[]            @default([])
  blocklistDomains String[]            @default([])
  /// Per-run timeout in ms.
  timeoutMs       Int                  @default(15000)
  lastValidatedAt DateTime?
  validationError String?
  createdAt       DateTime             @default(now())
  updatedAt       DateTime             @updatedAt

  runs SearchRun[]

  @@index([kind])
  @@index([status])
  @@map("search_providers")
}

/// A single search execution against a provider.
model SearchRun {
  id          String          @id @default(cuid())
  providerId  String
  provider    SearchProvider  @relation(fields: [providerId], references: [id], onDelete: Cascade)
  userId      String
  query       String
  status      SearchRunStatus @default(RUNNING)
  /// Number of normalized results returned.
  resultCount Int             @default(0)
  /// Latency in ms from start to completion.
  latencyMs   Int?
  errorMessage String?
  /// Full normalized result payload for later inspection.
  results     Json            @default("[]")
  /// Optional filters: provider-specific knobs (site, lang, freshness, etc).
  filters     Json            @default("{}")
  startedAt   DateTime        @default(now())
  completedAt DateTime?

  @@index([userId, startedAt])
  @@index([providerId, startedAt])
  @@map("search_runs")
}

/// A web page fetch for a given URL. We keep a cache and log each attempt.
model FetchJob {
  id          String         @id @default(cuid())
  userId      String
  /// Normalized URL (lowercased scheme+host, kept query + path as-is).
  url         String
  finalUrl    String?
  status      FetchJobStatus @default(RUNNING)
  httpStatus  Int?
  mimeType    String?
  /// Raw byte size (pre-extraction).
  byteSize    Int?
  /// Extracted clean content (truncated to MAX length).
  content     String?
  /// Extracted page title.
  title       String?
  /// Outbound links discovered on the page.
  links       String[]       @default([])
  /// Was the response served from the PageCache rather than fetched live?
  cacheHit    Boolean        @default(false)
  errorMessage String?
  latencyMs   Int?
  startedAt   DateTime       @default(now())
  completedAt DateTime?

  @@index([userId, startedAt])
  @@index([url, startedAt])
  @@map("fetch_jobs")
}

/// Shared cache of page content keyed by URL (or URL + variant).
model PageCache {
  /// Cache key: SHA256 of the normalized URL.
  key         String   @id
  url         String
  finalUrl    String?
  httpStatus  Int
  mimeType    String?
  title       String?
  content     String
  links       String[] @default([])
  byteSize    Int
  fetchedAt   DateTime @default(now())
  /// TTL expires-at; consumers must re-fetch beyond this instant.
  expiresAt   DateTime

  @@index([url])
  @@index([expiresAt])
  @@map("page_cache")
}

/// A research orchestration run — user intent, workflow, and the produced
/// evidence bundle. This is what chat-service passes to the final model.
model ResearchRun {
  id              String            @id @default(cuid())
  userId          String
  /// Model the user picked for the final answer — preserved through the run.
  requestedModel  String?
  requestedProvider String?
  workflow        ResearchWorkflowKind
  intent          String
  status          ResearchRunStatus @default(RUNNING)
  /// Array of normalized evidence items — see EvidenceBundle types.
  bundle          Json              @default("{}")
  /// Free-form trace entries: search/fetch/error phases with timings.
  trace           Json              @default("[]")
  errorMessage    String?
  startedAt       DateTime          @default(now())
  completedAt     DateTime?

  @@index([userId, startedAt])
  @@map("research_runs")
}
