/**
 * ReadMeABook Database Schema
 * Documentation: documentation/backend/database.md
 * ARCHITECTURE:
 * - audible_cache: Pure Audible metadata (popular/new releases from Audible.com)
 * - plex_library: Pure Plex library content (what's in your Plex server)
 * - audiobooks: User-requested audiobooks only (created on request)
 * - Matching happens at QUERY TIME by comparing audible_cache against plex_library
 */

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

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

// ============================================================================
// MODELS
// ============================================================================

model User {
  id           String    @id @default(uuid())
  plexId       String    @unique @map("plex_id")
  plexUsername String    @map("plex_username")
  plexEmail    String?   @map("plex_email")
  role         String    @default("user") // 'user' or 'admin'
  isSetupAdmin Boolean   @default(false) @map("is_setup_admin") // First admin created during setup, cannot be demoted
  avatarUrl    String?   @map("avatar_url")
  authToken    String?   @map("auth_token") // Encrypted
  createdAt    DateTime  @default(now()) @map("created_at")
  updatedAt    DateTime  @updatedAt @map("updated_at")
  lastLoginAt  DateTime? @map("last_login_at")

  // Plex Home profile tracking
  plexHomeUserId String? @map("plex_home_user_id") // Profile ID from Plex Home (null = main account, set = home profile)

  // Multi-auth support (for Audiobookshelf integration)
  authProvider       String? @map("auth_provider") // 'plex' | 'oidc' | 'local'
  oidcSubject        String? @map("oidc_subject") // OIDC subject ID (unique per provider)
  oidcProvider       String? @map("oidc_provider") // OIDC provider name (e.g., 'authentik', 'keycloak')
  registrationStatus String? @default("approved") @map("registration_status") // 'pending_approval' | 'approved' | 'rejected'

  // BookDate per-user preferences
  bookDateLibraryScope       String? @default("full") @map("bookdate_library_scope") // 'full' | 'rated' | 'favorites'
  bookDateFavoriteBookIds    String? @map("bookdate_favorite_book_ids") @db.Text // JSON array of PlexLibrary IDs (max 25)
  bookDateCustomPrompt       String? @map("bookdate_custom_prompt") @db.Text
  bookDateOnboardingComplete Boolean @default(false) @map("bookdate_onboarding_complete")

  // Request approval preferences
  autoApproveRequests Boolean? @map("auto_approve_requests") // null = use global setting, true = auto-approve, false = require approval

  // Fine-grained permissions
  interactiveSearchAccess Boolean? @map("interactive_search_access") // null = use global setting, true = allow, false = deny
  downloadAccess          Boolean? @map("download_access")           // null = use global setting, true = allow, false = deny

  // Login token (admin-generated, for direct URL login)
  loginTokenHash String? @map("login_token_hash") // SHA-256 hash of the login token (never store plaintext)

  // Session invalidation (set when login token is revoked to force-logout active sessions)
  sessionsInvalidatedAt DateTime? @map("sessions_invalidated_at")

  // Soft delete support
  deletedAt DateTime? @map("deleted_at")
  deletedBy String?   @map("deleted_by") // Admin user ID who deleted this user

  // Relations
  requests                Request[]
  bookDateRecommendations BookDateRecommendation[]
  bookDateSwipes          BookDateSwipe[]
  goodreadsShelves        GoodreadsShelf[]
  hardcoverShelves        HardcoverShelf[]
  reportedIssues          ReportedIssue[]          @relation("Reporter")
  resolvedIssues          ReportedIssue[]          @relation("Resolver")
  createdApiTokens        ApiToken[]  @relation("CreatedApiTokens")
  apiTokens               ApiToken[]  @relation("UserApiTokens")
  watchedSeries           WatchedSeries[]
  watchedAuthors          WatchedAuthor[]
  homeSections            UserHomeSection[]
  ignoredAudiobooks       IgnoredAudiobook[]

  @@index([plexId])
  @@index([role])
  @@index([deletedAt])
  @@map("users")
}

// ============================================================================
// AUDIBLE CACHE TABLE
// Pure Audible metadata - Popular/New Releases cached from Audible.com
// No Plex data, no availability status - just Audible metadata
// ============================================================================
model AudibleCache {
  id              String    @id @default(uuid())
  asin            String    @unique // Audible Standard Identification Number
  title           String
  author          String
  narrator        String?
  description     String?   @db.Text
  coverArtUrl     String?   @map("cover_art_url") @db.Text
  cachedCoverPath String?   @map("cached_cover_path") @db.Text // Local path to cached cover image
  durationMinutes Int?      @map("duration_minutes")
  releaseDate     DateTime? @map("release_date") @db.Date
  rating          Decimal?  @db.Decimal(3, 2)
  genres          Json      @default("[]")

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

  @@index([asin])
  @@index([title])
  @@index([author])
  @@map("audible_cache")
}

// ============================================================================
// LIBRARY CACHE TABLE (plex_library for backward compatibility)
// Universal library content - Works with Plex or Audiobookshelf backends
// Stores complete metadata including ASIN/ISBN for accurate matching
// No Audible data - just library backend metadata and file info
// ============================================================================
model PlexLibrary {
  id            String  @id @default(uuid())
  plexGuid      String  @unique @map("plex_guid") // Plex's unique identifier
  plexRatingKey String? @map("plex_rating_key") // Plex's rating key

  title      String
  author     String
  narrator   String?
  summary    String?  @db.Text
  duration   BigInt? // Duration in milliseconds (Plex format)
  year       Int?
  userRating Decimal? @map("user_rating") @db.Decimal(3, 1) // User's rating (0-10 scale from Plex)

  // Universal identifiers (works for both Plex and Audiobookshelf)
  asin String? // Audible ASIN - extracted from Plex GUID or stored directly from ABS
  isbn String? // ISBN (10 or 13) - for additional matching capability

  // File information
  filePath            String? @map("file_path") @db.Text
  thumbUrl            String? @map("thumb_url") @db.Text // Plex thumbnail URL
  cachedLibraryCoverPath String? @map("cached_library_cover_path") @db.Text // Local path to cached library cover image

  // Plex metadata
  plexLibraryId String    @map("plex_library_id") // Which Plex library contains this
  addedAt       DateTime? @map("added_at") // When added to Plex

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

  @@index([plexGuid])
  @@index([title])
  @@index([author])
  @@index([plexLibraryId])
  @@index([asin])
  @@index([isbn])
  @@map("plex_library")
}

// ============================================================================
// AUDIOBOOK TABLE (Simplified)
// Only created when user requests an audiobook
// Links to AudibleCache for metadata (optional - search results may not be cached)
// ============================================================================
model Audiobook {
  id String @id @default(uuid())

  // Core metadata (may come from Audible search, not necessarily cached)
  audibleAsin String? @map("audible_asin") // ASIN if from Audible
  title       String
  author      String
  narrator    String?
  description String? @db.Text
  coverArtUrl String? @map("cover_art_url") @db.Text
  year        Int?    // Release year extracted from releaseDate
  series      String? // Book series name (e.g., "The Mistborn Saga")
  seriesPart  String? @map("series_part") // Series position (e.g., "1", "1.5", "Book 1")
  seriesAsin  String? @map("series_asin") // Audible series ASIN for linking to series detail page

  // Request tracking
  status String @default("requested") // requested, downloading, processing, completed, failed

  // File information (populated after download/organization)
  filePath      String? @map("file_path") @db.Text
  fileFormat    String? @map("file_format") // m4b, m4a, mp3
  fileSizeBytes BigInt? @map("file_size_bytes")

  // Plex integration (populated after successful import)
  plexGuid      String? @map("plex_guid") // Set when imported into Plex
  plexLibraryId String? @map("plex_library_id")

  // Audiobookshelf integration (alternative to Plex)
  absItemId String? @map("abs_item_id") // Audiobookshelf item ID

  // File hash for accurate library matching (SHA256 of sorted audio filenames)
  filesHash String? @map("files_hash") @db.Text

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

  // Relations
  requests       Request[]
  reportedIssues ReportedIssue[]

  @@index([audibleAsin])
  @@index([plexGuid])
  @@index([absItemId])
  @@index([title])
  @@index([author])
  @@index([status])
  @@index([filesHash])
  @@map("audiobooks")
}

model Request {
  id               String    @id @default(uuid())
  userId           String    @map("user_id")
  audiobookId      String    @map("audiobook_id")
  status           String    @default("pending")
  // Status values: pending, awaiting_approval, denied, searching, downloading, processing, downloaded, available, failed, cancelled, awaiting_search, awaiting_import, awaiting_release, warn
  // Flow (audiobook): pending → searching → downloading → processing → downloaded → available (when matched in Plex)
  // Flow (ebook): pending → searching → downloading → processing → downloaded (terminal - no available state)
  progress         Int       @default(0) // 0-100
  priority         Int       @default(0)
  errorMessage     String?   @map("error_message") @db.Text
  selectedTorrent  Json?     @map("selected_torrent") // Pre-selected torrent from interactive search (stored when awaiting approval)
  searchAttempts   Int       @default(0) @map("search_attempts")
  downloadAttempts Int       @default(0) @map("download_attempts")
  importAttempts   Int       @default(0) @map("import_attempts")
  maxImportRetries Int       @default(5) @map("max_import_retries")
  ebookAutoRetryCount Int?   @map("ebook_auto_retry_count")
  lastSearchAt     DateTime? @map("last_search_at")
  customSearchTerms String?  @map("custom_search_terms") @db.Text
  lastImportAt     DateTime? @map("last_import_at")
  createdAt        DateTime  @default(now()) @map("created_at")
  updatedAt        DateTime  @updatedAt @map("updated_at")
  completedAt      DateTime? @map("completed_at")
  releaseDate      DateTime? @map("release_date") @db.Date // Book release date (copied from Audnexus on creation). Used by skip-unreleased-auto-search gate.

  // Request type: 'audiobook' (default) or 'ebook'
  // Ebook requests are created automatically when an audiobook is organized (if ebook downloads enabled)
  type             String    @default("audiobook") // 'audiobook' | 'ebook'
  parentRequestId  String?   @map("parent_request_id") // Links ebook request to originating audiobook request

  // Soft delete support
  deletedAt DateTime? @map("deleted_at")
  deletedBy String?   @map("deleted_by") // Admin user ID

  // Relations
  user            User              @relation(fields: [userId], references: [id], onDelete: Cascade)
  audiobook       Audiobook         @relation(fields: [audiobookId], references: [id], onDelete: Cascade)
  downloadHistory DownloadHistory[]
  jobs            Job[]
  parentRequest   Request?          @relation("EbookParent", fields: [parentRequestId], references: [id], onDelete: SetNull)
  childRequests   Request[]         @relation("EbookParent")
  blockedReleases BlockedRelease[]

  @@index([userId])
  @@index([audiobookId])
  @@index([status])
  @@index([createdAt(sort: Desc)])
  @@index([deletedAt])
  @@index([type])
  @@index([parentRequestId])
  @@map("requests")
}

// ============================================================================
// BLOCKED RELEASES TABLE
// Per-request blocklist of failed releases (organize-fail or download-fail).
// Search processors filter their candidate set against this table so future
// searches skip releases that have already failed for the same request.
// Documentation: documentation/backend/database.md
// ============================================================================

model BlockedRelease {
  id                String   @id @default(uuid())
  requestId         String   @map("request_id")
  releaseName       String   @map("release_name") @db.Text
  releaseKey        String   @map("release_key") @db.Text // normalized: trim + lowercase
  releaseHash       String?  @map("release_hash") // torrentHash OR nzbId (mutually exclusive in source)
  indexerName       String?  @map("indexer_name")
  indexerId         Int?     @map("indexer_id")
  source            String // 'organize_fail' | 'download_fail' | 'manual' (manual reserved for v2)
  reason            String   @db.Text // short reason, e.g. "No audiobook files found"
  reasonDetail      String?  @map("reason_detail") @db.Text // raw client error (SAB failMessage, NZBGet Par/Unpack)
  downloadHistoryId String?  @map("download_history_id") // traceability to the DownloadHistory row that failed
  jobId             String?  @map("job_id") // origin job (also drives JobEvent emission via logger)
  createdAt         DateTime @default(now()) @map("created_at")

  // Relations
  // Cascade: hard-delete of Request wipes its blocklist rows.
  // Soft-delete (Request.deletedAt) does NOT cascade — entries survive.
  request Request @relation(fields: [requestId], references: [id], onDelete: Cascade)

  @@unique([requestId, releaseKey]) // idempotency: one row per (request, normalized name)
  @@index([requestId])
  @@index([releaseKey])
  @@index([releaseHash])
  @@index([createdAt(sort: Desc)])
  @@map("blocked_releases")
}

model DownloadHistory {
  id               String    @id @default(uuid())
  requestId        String    @map("request_id")
  indexerName      String    @map("indexer_name")
  indexerId        Int?      @map("indexer_id") // Prowlarr indexer ID for configuration lookup
  torrentName      String?   @map("torrent_name")
  torrentHash      String?   @map("torrent_hash")
  nzbId            String?   @map("nzb_id") // SABnzbd NZB ID (mutually exclusive with torrentHash)
  torrentSizeBytes BigInt?   @map("torrent_size_bytes")
  magnetLink       String?   @map("magnet_link") @db.Text
  torrentUrl       String?   @map("torrent_url") @db.Text
  seeders          Int?
  leechers         Int?
  qualityScore     Int?      @map("quality_score")
  selected         Boolean   @default(false)
  downloadClient   String?   @map("download_client") // qbittorrent, sabnzbd, direct (HTTP download for ebooks)
  downloadClientId String?   @map("download_client_id")
  downloadStatus   String?   @map("download_status")
  // Status values: queued, downloading, completed, failed, stalled
  downloadError    String?   @map("download_error") @db.Text
  downloadPath     String?   @map("download_path") @db.Text
  startedAt        DateTime? @map("started_at")
  completedAt      DateTime? @map("completed_at")
  createdAt        DateTime  @default(now()) @map("created_at")

  // Relations
  request Request @relation(fields: [requestId], references: [id], onDelete: Cascade)

  @@index([requestId])
  @@index([selected])
  @@index([indexerId])
  @@index([torrentHash])
  @@index([nzbId])
  @@index([createdAt(sort: Desc)])
  @@map("download_history")
}

model Configuration {
  id          String   @id @default(uuid())
  key         String   @unique
  value       String?  @db.Text
  encrypted   Boolean  @default(false)
  category    String? // plex, indexer, download_client, system, automation
  description String?  @db.Text
  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")

  @@index([key])
  @@index([category])
  @@map("configuration")
}

model Job {
  id           String    @id @default(uuid())
  bullJobId    String?   @map("bull_job_id")
  requestId    String?   @map("request_id")
  type         String
  // Job types: search_indexers, monitor_download, organize_files, scan_plex, plex_recently_added_check, match_plex
  // Ebook job types: search_ebook, start_direct_download, monitor_direct_download
  status       String    @default("pending")
  // Status values: pending, active, completed, failed, delayed, stuck
  priority     Int       @default(0)
  attempts     Int       @default(0)
  maxAttempts  Int       @default(3) @map("max_attempts")
  payload      Json?
  result       Json?
  errorMessage String?   @map("error_message") @db.Text
  stackTrace   String?   @map("stack_trace") @db.Text
  startedAt    DateTime? @map("started_at")
  completedAt  DateTime? @map("completed_at")
  createdAt    DateTime  @default(now()) @map("created_at")
  updatedAt    DateTime  @updatedAt @map("updated_at")

  // Relations
  request Request?   @relation(fields: [requestId], references: [id], onDelete: SetNull)
  events  JobEvent[]

  @@index([requestId])
  @@index([type])
  @@index([status])
  @@index([createdAt(sort: Desc)])
  @@map("jobs")
}

model JobEvent {
  id        String   @id @default(uuid())
  jobId     String   @map("job_id")
  level     String // info, warn, error
  context   String // e.g., OrganizeFiles, FileOrganizer, MonitorDownload
  message   String   @db.Text
  metadata  Json? // Additional structured data
  createdAt DateTime @default(now()) @map("created_at")

  // Relations
  job Job @relation(fields: [jobId], references: [id], onDelete: Cascade)

  @@index([jobId])
  @@index([createdAt])
  @@map("job_events")
}

model ScheduledJob {
  id           String    @id @default(uuid())
  name         String
  type         String // 'plex_library_scan', 'plex_recently_added_check', 'audible_refresh', 'retry_missing_torrents', 'retry_failed_imports', 'cleanup_seeded_torrents', 'monitor_rss_feeds'
  schedule     String // Cron expression
  enabled      Boolean   @default(true)
  payload      Json      @default("{}")
  lastRun      DateTime? @map("last_run")
  lastRunJobId String?   @map("last_run_job_id") // Bull queue job ID of most recent execution
  nextRun      DateTime? @map("next_run")
  createdAt    DateTime  @default(now()) @map("created_at")
  updatedAt    DateTime  @updatedAt @map("updated_at")

  @@index([type])
  @@index([enabled])
  @@map("scheduled_jobs")
}

// ============================================================================
// BOOKDATE TABLES
// AI-powered audiobook recommendation system
// Documentation: documentation/features/bookdate-prd.md
// ============================================================================

model BookDateConfig {
  id           String   @id @default(uuid())
  provider     String // 'openai' | 'claude' | 'gemini' | 'custom'
  apiKey       String   @map("api_key") @db.Text // Encrypted at rest (AES-256)
  model        String // e.g., 'gpt-4o', 'claude-sonnet-4-5-20250929'
  baseUrl      String?  @map("base_url") @db.Text // Base URL for custom provider (OpenAI-compatible endpoints)
  isVerified   Boolean  @default(false) @map("is_verified")
  isEnabled    Boolean  @default(true) @map("is_enabled") // Admin toggle (global feature)
  createdAt    DateTime @default(now()) @map("created_at")
  updatedAt    DateTime @updatedAt @map("updated_at")

  @@map("bookdate_config")
}

// Note: BookDateConfig is now a singleton - only ONE record exists globally.
// Admin configures this in settings, and all users share the same API key.
// Individual users still have their own recommendations and swipe history.

model BookDateRecommendation {
  id           String   @id @default(uuid())
  userId       String   @map("user_id")
  batchId      String   @map("batch_id") // Group recommendations from same AI call
  title        String
  author       String
  narrator     String?
  rating       Decimal? @db.Decimal(3, 2)
  description  String?  @db.Text
  coverUrl     String?  @map("cover_url") @db.Text
  audnexusAsin String?  @map("audnexus_asin") // For matching
  aiReason     String   @map("ai_reason") @db.Text // Why AI recommended this
  createdAt    DateTime @default(now()) @map("created_at")

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

  @@index([userId, batchId])
  @@index([userId, createdAt])
  @@map("bookdate_recommendations")
}

model BookDateSwipe {
  id               String   @id @default(uuid())
  userId           String   @map("user_id")
  recommendationId String?  @map("recommendation_id") // NULL if book not from BookDate
  bookTitle        String   @map("book_title")
  bookAuthor       String   @map("book_author")
  action           String // 'left' | 'right' | 'up'
  markedAsKnown    Boolean  @default(false) @map("marked_as_known") // True if "Mark as Known"
  createdAt        DateTime @default(now()) @map("created_at")

  // Relations
  user           User                    @relation(fields: [userId], references: [id], onDelete: Cascade)
  recommendation BookDateRecommendation? @relation(fields: [recommendationId], references: [id], onDelete: SetNull)

  @@index([userId, createdAt])
  @@index([recommendationId])
  @@map("bookdate_swipes")
}

model NotificationBackend {
  id        String   @id @default(uuid())
  type      String   // 'discord' | 'pushover' | 'email' | 'slack' | 'telegram' | 'webhook'
  name      String   // User-friendly label
  config    Json     // Type-specific config (encrypted sensitive values)
  events    Json     @default("[]") // Array of event strings
  enabled   Boolean  @default(true)
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@index([enabled])
  @@map("notification_backends")
}

// ============================================================================
// REPORTED ISSUES TABLE
// User-reported problems with available audiobooks (corrupted, wrong book, etc.)
// ============================================================================

model ReportedIssue {
  id           String    @id @default(uuid())
  audiobookId  String    @map("audiobook_id")
  reporterId   String    @map("reporter_id")
  reason       String    @db.VarChar(250)
  status       String    @default("open") // open, dismissed, replaced
  resolvedAt   DateTime? @map("resolved_at")
  resolvedById String?   @map("resolved_by_id")
  createdAt    DateTime  @default(now()) @map("created_at")
  updatedAt    DateTime  @updatedAt @map("updated_at")

  // Relations
  audiobook  Audiobook @relation(fields: [audiobookId], references: [id], onDelete: Cascade)
  reporter   User      @relation("Reporter", fields: [reporterId], references: [id], onDelete: Cascade)
  resolvedBy User?     @relation("Resolver", fields: [resolvedById], references: [id], onDelete: SetNull)

  @@index([audiobookId])
  @@index([reporterId])
  @@index([status])
  @@map("reported_issues")
}

// ============================================================================
// GOODREADS SYNC TABLES
// Per-user Goodreads shelf subscriptions + global book-to-ASIN mapping cache
// ============================================================================

// ============================================================================
// API TOKEN TABLE
// Static API tokens for programmatic access (alternative to JWT)
// Documentation: documentation/backend/services/api-tokens.md
// ============================================================================

model ApiToken {
  id          String    @id @default(uuid())
  name        String // User-friendly label (e.g., "Home Assistant", "Webhook")
  tokenHash   String    @unique @map("token_hash") // SHA-256 hash of the token (never store plaintext)
  tokenPrefix String    @map("token_prefix") // First 8 chars for display (e.g., "rmab_a1b2")
  role        String    @default("user") // Token role: 'admin' or 'user'
  createdById String    @map("created_by_id") // Who created the token (may differ from userId for admin-created tokens)
  userId      String    @map("user_id") // The user identity this token acts as
  lastUsedAt  DateTime? @map("last_used_at")
  expiresAt   DateTime? @map("expires_at") // null = never expires
  createdAt   DateTime  @default(now()) @map("created_at")

  // Relations
  createdBy User @relation("CreatedApiTokens", fields: [createdById], references: [id], onDelete: Cascade)
  tokenUser User @relation("UserApiTokens", fields: [userId], references: [id], onDelete: Cascade)

  @@index([tokenHash])
  @@index([createdById])
  @@index([userId])
  @@map("api_tokens")
}

model GoodreadsShelf {
  id         String    @id @default(uuid())
  userId     String    @map("user_id")
  name       String    // Extracted from RSS <title>
  rssUrl     String    @map("rss_url") @db.Text
  lastSyncAt DateTime? @map("last_sync_at")
  bookCount  Int?      @map("book_count")
  coverUrls   String?   @map("cover_urls") @db.Text // JSON array of cover image URLs
  autoRequest Boolean   @default(true) @map("auto_request") // Whether to auto-create requests for books on this shelf
  createdAt   DateTime  @default(now()) @map("created_at")
  updatedAt   DateTime  @updatedAt @map("updated_at")

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

  @@unique([userId, rssUrl])
  @@index([userId])
  @@map("goodreads_shelves")
}

// ============================================================================
// UNIFIED BOOK MAPPING TABLE
// Global book-to-ASIN mapping cache shared across all shelf providers.
// Uses provider + externalBookId composite key for cross-provider dedup.
// ============================================================================

model BookMapping {
  id             String    @id @default(uuid())
  provider       String    // "goodreads", "hardcover", etc.
  externalBookId String    @map("external_book_id")
  title          String
  author         String
  audibleAsin    String?   @map("audible_asin")
  coverUrl       String?   @map("cover_url") @db.Text
  noMatch        Boolean   @default(false) @map("no_match")
  lastSearchAt   DateTime? @map("last_search_at")
  createdAt      DateTime  @default(now()) @map("created_at")
  updatedAt      DateTime  @updatedAt @map("updated_at")

  @@unique([provider, externalBookId])
  @@index([provider, externalBookId])
  @@index([audibleAsin])
  @@map("book_mappings")
}

// ============================================================================
// HARDCOVER SYNC TABLES
// Per-user Hardcover list subscriptions
// ============================================================================

model HardcoverShelf {
  id         String    @id @default(uuid())
  userId     String    @map("user_id")
  name       String    // Extracted from Hardcover API list name or status
  listId     String    @map("list_id") // Hardcover List ID or Status ID
  apiToken   String    @map("api_token") @db.Text // User's personal access token for hardcover api
  lastSyncAt DateTime? @map("last_sync_at")
  bookCount  Int?      @map("book_count")
  coverUrls   String?   @map("cover_urls") @db.Text // JSON array of cover image URLs
  autoRequest Boolean   @default(true) @map("auto_request") // Whether to auto-create requests for books on this shelf
  createdAt   DateTime  @default(now()) @map("created_at")
  updatedAt   DateTime  @updatedAt @map("updated_at")

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

  @@unique([userId, listId])
  @@index([userId])
  @@map("hardcover_shelves")
}

// ============================================================================
// WORKS TABLE
// Cross-ASIN audiobook identity mapping — links multiple Audible ASINs
// to a single logical work for library matching across editions.
// Documentation: documentation/integrations/audible.md
// ============================================================================

model Work {
  id        String     @id @default(uuid())
  title     String
  author    String
  createdAt DateTime   @default(now()) @map("created_at")
  updatedAt DateTime   @updatedAt @map("updated_at")

  // Relations
  asins WorkAsin[]

  @@index([title])
  @@index([author])
  @@map("works")
}

model WorkAsin {
  id              String   @id @default(uuid())
  workId          String   @map("work_id")
  asin            String   @unique
  narrator        String?
  durationMinutes Int?     @map("duration_minutes")
  isCanonical     Boolean  @default(false) @map("is_canonical")
  source          String   // 'dedup_auto' | 'admin_manual'
  createdAt       DateTime @default(now()) @map("created_at")

  // Relations
  work Work @relation(fields: [workId], references: [id], onDelete: Cascade)

  @@index([workId])
  @@index([asin])
  @@map("work_asins")
}

// ============================================================================
// WATCHED LISTS TABLES
// Per-user series and author subscriptions for automatic new-release requests.
// Documentation: documentation/features/watched-lists.md
// ============================================================================

model WatchedSeries {
  id            String    @id @default(uuid())
  userId        String    @map("user_id")
  seriesAsin    String    @map("series_asin")
  seriesTitle   String    @map("series_title")
  coverArtUrl   String?   @map("cover_art_url") @db.Text
  lastCheckedAt DateTime? @map("last_checked_at")
  createdAt     DateTime  @default(now()) @map("created_at")
  updatedAt     DateTime  @updatedAt @map("updated_at")

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

  @@unique([userId, seriesAsin])
  @@index([userId])
  @@index([seriesAsin])
  @@map("watched_series")
}

model WatchedAuthor {
  id            String    @id @default(uuid())
  userId        String    @map("user_id")
  authorAsin    String    @map("author_asin")
  authorName    String    @map("author_name")
  coverArtUrl   String?   @map("cover_art_url") @db.Text
  lastCheckedAt DateTime? @map("last_checked_at")
  createdAt     DateTime  @default(now()) @map("created_at")
  updatedAt     DateTime  @updatedAt @map("updated_at")

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

  @@unique([userId, authorAsin])
  @@index([userId])
  @@index([authorAsin])
  @@map("watched_authors")
}

// ============================================================================
// IGNORED AUDIOBOOK TABLE
// Per-user ignore list for auto-request suppression.
// Stores the ASIN the user clicked ignore on; works-system expansion
// happens at check-time in request-creator.service.ts.
// Documentation: documentation/features/ignored-audiobooks.md
// ============================================================================

model IgnoredAudiobook {
  id          String   @id @default(uuid())
  userId      String   @map("user_id")
  asin        String   // Audible ASIN that was explicitly ignored
  title       String   // Display only — snapshot at ignore time
  author      String   // Display only — snapshot at ignore time
  coverArtUrl String?  @map("cover_art_url") @db.Text
  createdAt   DateTime @default(now()) @map("created_at")

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

  @@unique([userId, asin])
  @@index([userId])
  @@index([asin])
  @@map("ignored_audiobooks")
}

// ============================================================================
// USER HOME SECTION TABLE
// Per-user configurable home page sections (popular, new_releases, category)
// Documentation: documentation/features/home-sections.md
// ============================================================================

model UserHomeSection {
  id           String   @id @default(uuid())
  userId       String   @map("user_id")
  sectionType  String   @map("section_type") // 'popular' | 'new_releases' | 'category'
  categoryId   String?  @map("category_id") // Audible category node ID (only for type 'category')
  categoryName String?  @map("category_name") // Display name (only for type 'category')
  sortOrder    Int      @map("sort_order")
  createdAt    DateTime @default(now()) @map("created_at")
  updatedAt    DateTime @updatedAt @map("updated_at")

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

  @@unique([userId, sectionType, categoryId])
  @@index([userId])
  @@index([sortOrder])
  @@map("user_home_sections")
}

// ============================================================================
// AUDIBLE CACHE CATEGORY TABLE
// Join table linking AudibleCache entries to Audible categories with ranking
// Documentation: documentation/features/home-sections.md
// ============================================================================

model AudibleCacheCategory {
  id           String   @id @default(uuid())
  asin         String
  categoryId   String   @map("category_id")
  rank         Int
  lastSyncedAt DateTime @default(now()) @map("last_synced_at")
  createdAt    DateTime @default(now()) @map("created_at")

  @@unique([asin, categoryId])
  @@index([categoryId])
  @@index([asin])
  @@index([categoryId, rank])
  @@map("audible_cache_categories")
}

// ============================================================================
// DATA MIGRATION TRACKING
// Tracks which data migration SQL scripts have been executed (run-once).
// ============================================================================

model DataMigration {
  name       String   @id
  executedAt DateTime @default(now()) @map("executed_at")

  @@map("_data_migrations")
}
