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

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

model User {
  id                     String   @id @default(cuid())
  username               String   @unique
  passwordHash           String
  role                   String   @default("user") // user, admin
  onboardingComplete     Boolean  @default(false) // Tracks if user completed setup
  enrichmentSettings     Json? // JSON settings for metadata enrichment
  twoFactorEnabled       Boolean  @default(false) // 2FA enabled flag
  twoFactorSecret        String? // TOTP secret (encrypted)
  twoFactorRecoveryCodes String? // Recovery codes (encrypted, comma-separated hashed codes)
  moodMixParams          Json? // Saved mood mix parameters for "Your Mood Mix"
  tokenVersion           Int      @default(0) // Incremented on password change to invalidate tokens
  createdAt              DateTime @default(now())

  plays                Play[]
  playlists            Playlist[]
  listeningState       ListeningState[]
  downloadJobs         DownloadJob[]
  spotifyImportJobs    SpotifyImportJob[]
  settings             UserSettings?
  playbackState        PlaybackState?
  likedTracks          LikedTrack[]
  dislikedEntities     DislikedEntity[]
  cachedTracks         CachedTrack[]
  audiobookProgress    AudiobookProgress[]
  podcastSubscriptions PodcastSubscription[]
  podcastProgress      PodcastProgress[]
  podcastDownloads     PodcastDownload[]
  discoveryAlbums      DiscoveryAlbum[]
  discoverExclusions   DiscoverExclusion[]
  discoverConfig       UserDiscoverConfig?
  unavailableAlbums    UnavailableAlbum[]
  apiKeys              ApiKey[]
  deviceLinkCodes      DeviceLinkCode[]
  hiddenPlaylists      HiddenPlaylist[]
  notifications        Notification[]
  userMoodMix          UserMoodMix?
}

model UserSettings {
  userId          String  @id
  playbackQuality String  @default("original") // original, high, medium, low
  wifiOnly        Boolean @default(false)
  offlineEnabled  Boolean @default(false)
  maxCacheSizeMb  Int     @default(10240) // 10GB default

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

model PlaybackState {
  userId       String   @id
  playbackType String // track, audiobook, podcast
  trackId      String? // For music tracks
  audiobookId  String? // For audiobooks (audiobookshelfId)
  podcastId    String? // For podcasts (format: podcastId:episodeId)
  queue        Json? // JSON array of track IDs
  currentIndex Int      @default(0)
  isShuffle    Boolean  @default(false)
  updatedAt    DateTime @updatedAt
  createdAt    DateTime @default(now())

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

  @@index([userId])
}

model SystemSettings {
  id String @id @default("default")

  // === Download Services ===
  // Lidarr
  lidarrEnabled     Boolean @default(true)
  lidarrUrl         String? @default("http://localhost:8686")
  lidarrApiKey      String? // Encrypted
  lidarrWebhookSecret String? // Encrypted - Shared secret for webhook verification

  // === AI Services ===
  // OpenAI (for future AI features)
  openaiEnabled Boolean @default(false)
  openaiApiKey  String? // Encrypted
  openaiModel   String? @default("gpt-4")
  openaiBaseUrl String? // For custom endpoints

  // Fanart.tv (for high-quality images - optional)
  fanartEnabled Boolean @default(false)
  fanartApiKey  String? // Encrypted

  // Last.fm (optional user override - app ships with default key)
  lastfmApiKey  String? // Encrypted

  // === Media Services ===
  // Audiobookshelf
  audiobookshelfEnabled Boolean @default(false)
  audiobookshelfUrl     String? @default("http://localhost:13378")
  audiobookshelfApiKey  String? // Encrypted

  // Soulseek (direct connection via slsk-client)
  soulseekUsername String? // Soulseek network username
  soulseekPassword String? // Soulseek network password - Encrypted

  // Spotify (for playlist import via URL)
  spotifyClientId     String?
  spotifyClientSecret String? // Encrypted

  // === Storage Paths ===
  musicPath    String? @default("/music")
  downloadPath String? @default("/downloads")

  // === Feature Flags ===
  autoSync           Boolean @default(true)
  autoEnrichMetadata Boolean @default(true)

  // === Advanced Settings ===
  maxConcurrentDownloads Int @default(3)
  downloadRetryAttempts  Int @default(3)
  transcodeCacheMaxGb    Int @default(10) // Transcode cache size limit in GB
  enrichmentConcurrency  Int @default(1)  // 1-5, number of parallel enrichment workers
  audioAnalyzerWorkers   Int @default(2)  // 1-8, number of parallel audio analysis workers
  soulseekConcurrentDownloads Int @default(4)  // 1-10, concurrent Soulseek downloads

  // === Download Preferences ===
  // Primary download source: "soulseek" (per-track) or "lidarr" (full albums)
  downloadSource          String @default("soulseek")
  // Fallback when primary source fails: "none" (skip), "lidarr" (full album), or "soulseek" (track-based)
  primaryFailureFallback  String @default("none")

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

model Artist {
  id                   String                   @id @default(cuid())
  mbid                 String                   @unique
  name                 String
  normalizedName       String                   @default("") // Lowercase version for case-insensitive matching
  summary              String?                  @db.Text
  heroUrl              String?
  genres               Json? // Array of genre strings from Last.fm/MusicBrainz
  similarArtistsJson   Json? // Full Last.fm similar artists data [{name, mbid, match, image}]
  lastSynced           DateTime                 @default(now())
  lastEnriched         DateTime?
  enrichmentStatus     String                   @default("pending") // pending, enriching, completed, failed
  searchVector         Unsupported("tsvector")?

  // User overrides (optional, takes display precedence)
  displayName      String? // User-provided display name
  userSummary      String? @db.Text // User-provided bio
  userHeroUrl      String? // User-uploaded/linked image
  userGenres       Json? // User-modified genres (array of strings)
  hasUserOverrides Boolean @default(false) // Quick check flag

  albums      Album[]
  similarFrom SimilarArtist[] @relation("FromArtist")
  similarTo   SimilarArtist[] @relation("ToArtist")
  ownedAlbums OwnedAlbum[]

  @@index([name])
  @@index([normalizedName])
  @@index([searchVector], type: Gin)
  @@index([hasUserOverrides])
}

model Album {
  id           String                   @id @default(cuid())
  rgMbid       String                   @unique // release group MBID
  artistId     String
  title        String
  year         Int? // File metadata date (may be remaster)
  originalYear Int? // Original release date from MusicBrainz
  coverUrl     String?
  primaryType  String // Album, EP, Single, Live, Compilation
  label        String? // Record label (from MusicBrainz)
  genres       Json? // Array of genre strings from Last.fm
  lastSynced   DateTime                 @default(now())
  location     AlbumLocation            @default(LIBRARY) // LIBRARY or DISCOVER
  searchVector Unsupported("tsvector")?

  // User overrides (optional, takes display precedence)
  displayTitle     String? // User-provided display title
  displayYear      Int? // User-provided year
  userCoverUrl     String? // User-uploaded/linked cover
  userGenres       Json? // User-modified genres (array of strings)
  hasUserOverrides Boolean @default(false) // Quick check flag

  artist Artist  @relation(fields: [artistId], references: [id], onDelete: Cascade)
  tracks Track[]

  @@index([artistId])
  @@index([location])
  @@index([title])
  @@index([searchVector], type: Gin)
  @@index([hasUserOverrides])
}

model Track {
  id           String                   @id @default(cuid())
  albumId      String
  title        String
  trackNo      Int
  duration     Int // seconds
  mime         String?
  searchVector Unsupported("tsvector")?

  // Native file system fields (required for self-contained system)
  filePath     String   @unique // Relative path: "Artist/Album/track.flac"
  fileModified DateTime // mtime for change detection
  fileSize     Int // File size in bytes

  // User overrides (optional, takes display precedence)
  displayTitle     String? // User-provided display title
  displayTrackNo   Int? // User-provided track number
  hasUserOverrides Boolean @default(false) // Quick check flag

  // === Audio Analysis (Essentia) ===
  // Rhythm
  bpm        Float? // Beats per minute (e.g., 120.5)
  beatsCount Int? // Total beats in track

  // Tonality  
  key         String? // Musical key (e.g., "C", "F#", "Bb")
  keyScale    String? // "major" or "minor"
  keyStrength Float? // Confidence 0-1

  // Energy & Dynamics
  energy       Float? // Overall energy 0-1
  loudness     Float? // Average loudness in dB
  dynamicRange Float? // Dynamic range in dB

  // Mood & Character (basic/estimated)
  danceability Float? // 0-1 how suitable for dancing
  valence      Float? // 0 (sad) to 1 (happy) - ML in Enhanced mode
  arousal      Float? // 0 (calm) to 1 (energetic) - ML in Enhanced mode

  // Instrumentation
  instrumentalness Float? // 0-1 (1 = no vocals) - ML in Enhanced mode
  acousticness     Float? // 0-1 (1 = acoustic)
  speechiness      Float? // 0-1 (1 = spoken word)

  // === Enhanced Mode: ML Mood Predictions ===
  moodHappy      Float? // ML prediction 0-1 (probability of happy)
  moodSad        Float? // ML prediction 0-1 (probability of sad)
  moodRelaxed    Float? // ML prediction 0-1 (probability of relaxed)
  moodAggressive Float? // ML prediction 0-1 (probability of aggressive)
  moodParty      Float? // ML prediction 0-1 (probability of party/upbeat)
  moodAcoustic   Float? // ML prediction 0-1 (probability of acoustic)
  moodElectronic Float? // ML prediction 0-1 (probability of electronic)
  danceabilityMl Float? // ML-based danceability (more accurate than basic)

  // Mood Tags (derived from ML or heuristics)
  moodTags String[] // ["aggressive", "happy", "sad", "relaxed"]

  // Genre (ML classification from Essentia, backup to Last.fm)
  essentiaGenres String[] // ["rock", "electronic", "jazz"]

  // Last.fm Tags (user-generated mood/vibe tags)
  lastfmTags String[] // ["chill", "workout", "sad", "90s"]

  // Analysis Metadata
  analysisStatus     String    @default("pending") // pending, processing, completed, failed
  analysisStartedAt  DateTime? // When processing began (for timeout detection)
  analysisVersion    String?   // Essentia version used
  analysisMode       String?   // 'standard' or 'enhanced'
  analyzedAt         DateTime?
  analysisError      String?   // Error message if failed
  analysisRetryCount Int       @default(0) // Number of retry attempts
  updatedAt          DateTime  @updatedAt

  album           Album            @relation(fields: [albumId], references: [id], onDelete: Cascade)
  plays           Play[]
  playlistItems   PlaylistItem[]
  trackGenres     TrackGenre[]
  likedBy         LikedTrack[]
  cachedBy        CachedTrack[]
  transcodedFiles TranscodedFile[]
  moodBuckets     MoodBucket[]

  @@index([albumId])
  @@index([fileModified])
  @@index([title])
  @@index([searchVector], type: Gin)
  @@index([analysisStatus])
  @@index([analysisMode])
  @@index([bpm])
  @@index([energy])
  @@index([valence])
  @@index([danceability])
  @@index([moodHappy])
  @@index([moodSad])
  @@index([moodRelaxed])
  @@index([moodAggressive])
  @@index([moodParty])
  @@index([moodAcoustic])
  @@index([moodElectronic])
  @@index([arousal])
  @@index([acousticness])
  @@index([instrumentalness])
  @@index([hasUserOverrides])
}

// Transcoded file cache for audio streaming
model TranscodedFile {
  id             String   @id @default(cuid())
  trackId        String
  quality        String // original, high, medium, low
  cachePath      String   @unique // Relative path in transcode cache
  cacheSize      Int // File size in bytes
  sourceModified DateTime // For invalidation
  lastAccessed   DateTime @default(now()) // For LRU eviction
  createdAt      DateTime @default(now())

  track Track @relation(fields: [trackId], references: [id], onDelete: Cascade)

  @@unique([trackId, quality])
  @@index([trackId, quality])
  @@index([lastAccessed])
}

model Play {
  id       String       @id @default(cuid())
  userId   String
  trackId  String
  playedAt DateTime     @default(now())
  source   ListenSource @default(LIBRARY) // LIBRARY, DISCOVERY, or DISCOVERY_KEPT

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

  @@index([userId, playedAt])
  @@index([trackId])
  @@index([source])
}

model Playlist {
  id        String   @id @default(cuid())
  userId    String
  mixId     String?
  name      String
  isPublic  Boolean  @default(false)
  createdAt DateTime @default(now())
  
  // Spotify import metadata
  spotifyPlaylistId  String?  // Original Spotify playlist ID
  spotifyPlaylistUrl String?  // Original Spotify URL for re-import

  user           User                   @relation(fields: [userId], references: [id], onDelete: Cascade)
  items          PlaylistItem[]
  pendingTracks  PlaylistPendingTrack[]
  hiddenByUsers  HiddenPlaylist[]

  @@unique([userId, mixId])
  @@index([userId])
  @@index([spotifyPlaylistId])
}

// Track which users have hidden which shared playlists
model HiddenPlaylist {
  id         String   @id @default(cuid())
  userId     String
  playlistId String
  createdAt  DateTime @default(now())

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

  @@unique([userId, playlistId])
  @@index([userId])
}

model PlaylistItem {
  id         String @id @default(cuid())
  playlistId String
  trackId    String
  sort       Int

  playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)
  track    Track    @relation(fields: [trackId], references: [id], onDelete: Cascade)

  @@unique([playlistId, trackId])
  @@index([playlistId, sort])
}

// Tracks from Spotify imports that haven't been matched to local library yet
// These are automatically added to the playlist when the matching track is downloaded
model PlaylistPendingTrack {
  id               String   @id @default(cuid())
  playlistId       String
  spotifyArtist    String   // Original artist name from Spotify
  spotifyTitle     String   // Original track title from Spotify
  spotifyAlbum     String   // Original album name from Spotify
  albumMbid        String?  // MusicBrainz album ID if resolved
  artistMbid       String?  // MusicBrainz artist ID if resolved
  deezerPreviewUrl String?  // Deezer 30s preview URL for playback while pending
  sort             Int      // Position in original playlist
  createdAt        DateTime @default(now())

  playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade)

  @@unique([playlistId, spotifyArtist, spotifyTitle]) // Prevent duplicates
  @@index([playlistId])
  @@index([albumMbid])
  @@index([artistMbid])
}

// Spotify Import Jobs - tracks import progress and state
model SpotifyImportJob {
  id                  String   @id
  userId              String
  spotifyPlaylistId   String
  playlistName        String
  status              String   // pending, downloading, scanning, creating_playlist, completed, failed, cancelled
  progress            Int      @default(0) // 0-100
  albumsTotal         Int
  albumsCompleted     Int      @default(0)
  tracksTotal         Int
  tracksMatched       Int      @default(0)
  tracksDownloadable  Int      @default(0)
  createdPlaylistId   String?
  error               String?
  pendingTracks       Json     // Array of pending track objects
  createdAt           DateTime @default(now())
  updatedAt           DateTime @updatedAt

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

  @@index([userId])
  @@index([status])
  @@index([createdAt])
}

model Genre {
  id          String       @id @default(cuid())
  name        String       @unique
  trackGenres TrackGenre[]
}

model TrackGenre {
  trackId String
  genreId String

  track Track @relation(fields: [trackId], references: [id], onDelete: Cascade)
  genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade)

  @@id([trackId, genreId])
  @@index([genreId])
}

model SimilarArtist {
  fromArtistId String
  toArtistId   String
  weight       Float  @default(1.0)

  fromArtist Artist @relation("FromArtist", fields: [fromArtistId], references: [id], onDelete: Cascade)
  toArtist   Artist @relation("ToArtist", fields: [toArtistId], references: [id], onDelete: Cascade)

  @@id([fromArtistId, toArtistId])
  @@index([fromArtistId])
}

model OwnedAlbum {
  artistId String
  rgMbid   String
  source   String // lidarr, manual, native_scan

  artist Artist @relation(fields: [artistId], references: [id], onDelete: Cascade)

  @@id([artistId, rgMbid])
  @@index([artistId])
}

model DownloadJob {
  id               String    @id @default(cuid())
  correlationId    String?   @unique // UUID for reliable webhook matching
  userId           String
  subject          String // artist name or album title
  type             String // artist, album
  targetMbid       String // artist MBID or release group MBID
  status           String // pending, processing, completed, failed, exhausted
  error            String? // error message if failed
  lidarrRef        String? // Lidarr's downloadId from webhook
  lidarrAlbumId    Int? // Lidarr's internal album ID for retry/cleanup
  metadata         Json? // additional metadata (downloadType, rootFolderPath, etc.)
  attempts         Int       @default(0) // Number of download attempts
  startedAt        DateTime? // When download was initiated (for timeout tracking)
  completedAt      DateTime?
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt
  discoveryBatchId String? // Links to Discovery Weekly batch if part of discovery

  // Release iteration tracking (for exhaustive retry)
  triedReleases    String[]  @default([]) // GUIDs of releases we've tried
  releaseIndex     Int       @default(0)   // Current position in release list
  artistMbid       String?   // Artist MBID for same-artist fallback
  cleared          Boolean   @default(false) // User dismissed from history

  user           User             @relation(fields: [userId], references: [id], onDelete: Cascade)
  discoveryBatch DiscoveryBatch? @relation(fields: [discoveryBatchId], references: [id], onDelete: SetNull)

  @@index([userId, status])
  @@index([status])
  @@index([discoveryBatchId])
  @@index([correlationId])
  @@index([startedAt])
  @@index([lidarrRef])
  @@index([artistMbid])
  @@index([targetMbid])
}

model ListeningState {
  id         String   @id @default(cuid())
  userId     String
  kind       String // music, book
  entityId   String // artist/album/book ID
  trackId    String? // current track/chapter
  positionMs Int      @default(0)
  updatedAt  DateTime @updatedAt

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

  @@unique([userId, kind, entityId])
  @@index([userId])
}

model DiscoveryAlbum {
  id            String         @id @default(cuid())
  userId        String
  rgMbid        String
  artistName    String
  artistMbid    String?
  albumTitle    String
  lidarrAlbumId Int?
  downloadedAt  DateTime?
  folderPath    String         @default("")
  weekStartDate DateTime // When it was added to discovery
  weekEndDate   DateTime       @default(now()) // When it expires
  status        DiscoverStatus @default(ACTIVE)
  likedAt       DateTime? // When user liked it
  similarity    Float? // Similarity score from Last.fm (0-1)
  tier          String? // high, medium, low, wild

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

  @@unique([userId, weekStartDate, rgMbid])
  @@index([userId, weekStartDate])
  @@index([downloadedAt])
  @@index([status])
}

model DiscoveryTrack {
  id               String    @id @default(cuid())
  discoveryAlbumId String
  trackId          String?
  fileName         String
  filePath         String
  inPlaylistCount  Int       @default(0)
  userKept         Boolean   @default(false)
  lastPlayedAt     DateTime?

  discoveryAlbum DiscoveryAlbum @relation(fields: [discoveryAlbumId], references: [id], onDelete: Cascade)

  @@index([discoveryAlbumId])
  @@index([userKept])
  @@index([lastPlayedAt])
}

model LikedTrack {
  userId  String
  trackId String
  likedAt DateTime @default(now())

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

  @@id([userId, trackId])
  @@index([userId])
  @@index([likedAt])
}

model DislikedEntity {
  id         String   @id @default(cuid())
  userId     String
  entityType String // track, album, artist
  entityId   String // trackId, albumId, artistId
  dislikedAt DateTime @default(now())

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

  @@unique([userId, entityType, entityId])
  @@index([userId, entityType])
}

model CachedTrack {
  id             String   @id @default(cuid())
  userId         String
  trackId        String
  localPath      String
  quality        String // original, high, medium, low
  fileSizeMb     Float
  cachedAt       DateTime @default(now())
  lastAccessedAt DateTime @default(now())

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

  @@unique([userId, trackId, quality])
  @@index([userId])
  @@index([lastAccessedAt])
}

model AudiobookProgress {
  id               String   @id @default(cuid())
  userId           String
  audiobookshelfId String // The audiobook ID from Audiobookshelf
  title            String // Cached for display
  author           String? // Cached for display
  coverUrl         String? // Cached for display
  currentTime      Float    @default(0) // Current playback position in seconds
  duration         Float    @default(0) // Total duration in seconds
  isFinished       Boolean  @default(false)
  lastPlayedAt     DateTime @default(now())
  updatedAt        DateTime @updatedAt
  createdAt        DateTime @default(now())

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  // Note: No foreign key to Audiobook - progress can exist before audiobook is cached

  @@unique([userId, audiobookshelfId])
  @@index([userId, lastPlayedAt])
}

// ============================================
// Cached Audiobooks (from Audiobookshelf)
// ============================================

model Audiobook {
  id            String  @id // Audiobookshelf item ID
  title         String
  author        String?
  narrator      String?
  description   String? @db.Text
  publishedYear Int?
  publisher     String?

  // Series info
  series         String?
  seriesSequence String?

  // Media info
  duration    Float? // seconds
  numTracks   Int?
  numChapters Int?
  size        BigInt? // bytes

  // Metadata
  isbn     String?
  asin     String?
  language String?
  genres   String[] // array of genres
  tags     String[] // array of tags

  // Files
  localCoverPath String? // local cached cover image path
  coverUrl       String? // original Audiobookshelf URL
  audioUrl       String // Audiobookshelf streaming URL
  libraryId      String? // Audiobookshelf library ID

  // Full-text search
  searchVector Unsupported("tsvector")?

  // Timestamps
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  lastSyncedAt DateTime @default(now())

  @@index([title])
  @@index([author])
  @@index([series])
  @@index([lastSyncedAt])
  @@index([searchVector], type: Gin)
}

model PodcastRecommendation {
  id            String   @id @default(cuid())
  podcastId     String // The source podcast ID (from Audiobookshelf)
  recommendedId String // Unique ID for the recommended podcast
  title         String
  author        String?
  description   String?  @db.Text
  coverUrl      String?
  episodeCount  Int      @default(0)
  feedUrl       String?
  itunesId      String?
  score         Float    @default(0) // Relevance score
  cachedAt      DateTime @default(now())
  expiresAt     DateTime // When this recommendation expires (30 days from cache)

  @@index([podcastId, expiresAt])
  @@index([expiresAt]) // For cleanup of expired recommendations
  @@map("podcast_recommendations")
}

// ============================================
// NEW: Independent Podcast System (RSS-based)
// ============================================

model Podcast {
  id              String                   @id @default(cuid())
  feedUrl         String                   @unique
  title           String
  author          String?
  description     String?                  @db.Text
  imageUrl        String?                  // Original feed image URL
  localCoverPath  String?                  // Local cached cover image path
  itunesId        String?                  @unique
  language        String?
  explicit        Boolean                  @default(false)
  episodeCount    Int                      @default(0)
  lastRefreshed   DateTime                 @default(now())
  refreshInterval Int                      @default(3600) // seconds (1 hour default)
  autoRefresh     Boolean                  @default(true)
  createdAt       DateTime                 @default(now())
  updatedAt       DateTime                 @updatedAt
  searchVector    Unsupported("tsvector")?

  episodes      PodcastEpisode[]
  subscriptions PodcastSubscription[]

  @@index([itunesId])
  @@index([lastRefreshed])
  @@index([searchVector], type: Gin)
}

model PodcastEpisode {
  id             String                   @id @default(cuid())
  podcastId      String
  guid           String                   // RSS GUID (unique per feed)
  title          String
  description    String?                  @db.Text
  audioUrl       String                   // Direct MP3/audio URL from RSS
  duration       Int                      @default(0) // seconds
  publishedAt    DateTime
  episodeNumber  Int?
  season         Int?
  imageUrl       String?                  // Episode-specific image URL
  localCoverPath String?                  // Local cached episode cover
  fileSize       Int?                     // bytes
  mimeType       String?                  @default("audio/mpeg")
  createdAt      DateTime                 @default(now())
  searchVector   Unsupported("tsvector")?

  podcast   Podcast           @relation(fields: [podcastId], references: [id], onDelete: Cascade)
  progress  PodcastProgress[]
  downloads PodcastDownload[]

  @@unique([podcastId, guid])
  @@index([podcastId, publishedAt])
  @@index([searchVector], type: Gin)
}

// User podcast subscriptions
model PodcastSubscription {
  userId       String
  podcastId    String
  subscribedAt DateTime @default(now())

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

  @@id([userId, podcastId])
  @@index([userId])
  @@index([podcastId])
}

// Listening progress for podcast episodes
model PodcastProgress {
  id           String   @id @default(cuid())
  userId       String
  episodeId    String
  currentTime  Float    @default(0) // seconds
  duration     Float    @default(0) // seconds
  isFinished   Boolean  @default(false)
  lastPlayedAt DateTime @default(now())
  updatedAt    DateTime @updatedAt
  createdAt    DateTime @default(now())

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

  @@unique([userId, episodeId])
  @@index([userId, lastPlayedAt])
}

// Downloaded episodes for offline playback
model PodcastDownload {
  id             String   @id @default(cuid())
  userId         String
  episodeId      String
  localPath      String // Where the file is stored locally
  fileSizeMb     Float
  downloadedAt   DateTime @default(now())
  lastAccessedAt DateTime @default(now())

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

  @@unique([userId, episodeId])
  @@index([userId])
  @@index([lastAccessedAt]) // For cache cleanup
}

// ============================================
// Discover Weekly System
// ============================================

// Album exclusion tracking - prevents suggesting the same album for 6 months
model DiscoverExclusion {
  id              String   @id @default(cuid())
  userId          String
  albumMbid       String // MusicBrainz release group ID
  artistName      String?  // For display purposes
  albumTitle      String?  // For display purposes
  lastSuggestedAt DateTime @default(now())
  expiresAt       DateTime // 6 months from lastSuggestedAt

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

  @@unique([userId, albumMbid])
  @@index([userId, expiresAt])
}

// User configuration for Discover Weekly
model UserDiscoverConfig {
  id               String    @id @default(cuid())
  userId           String    @unique
  playlistSize     Int       @default(40) // 5-50, increments of 5
  maxRetryAttempts Int       @default(3) // 1-10, how many times to retry finding replacements
  exclusionMonths  Int       @default(6) // 0-12, months to exclude albums after download (0 = no exclusion)
  downloadRatio    Float     @default(1.3) // 1.0-2.0, multiplier for albums to request vs target songs
  enabled          Boolean   @default(true)
  lastGeneratedAt  DateTime?

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

// Unavailable albums (recommended but not found in Lidarr)
model UnavailableAlbum {
  id              String   @id @default(cuid())
  userId          String
  artistName      String
  albumTitle      String
  albumMbid       String
  artistMbid      String?
  similarity      Float // Similarity score from Last.fm (0-1)
  tier            String // high, medium, low, wild
  weekStartDate   DateTime // When it was recommended
  previewUrl      String? // 30-second preview from Deezer
  deezerTrackId   String? // Deezer track ID for preview
  deezerAlbumId   String? // Deezer album ID for preview
  attemptNumber   Int      @default(0) // 0 = original, 1 = first replacement, 2 = second replacement, etc.
  originalAlbumId String? // References the original album's ID if this is a replacement
  createdAt       DateTime @default(now())

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

  @@unique([userId, weekStartDate, albumMbid])
  @@index([userId, weekStartDate])
  @@index([userId, weekStartDate, attemptNumber])
  @@index([originalAlbumId])
}

// Batch tracking for Discovery Weekly generation
model DiscoveryBatch {
  id              String    @id @default(cuid())
  userId          String
  weekStart       DateTime // Week this batch is for
  targetSongCount Int // Target number of songs to find
  status          String    @default("downloading") // downloading, scanning, completed, failed
  totalAlbums     Int       @default(0) // Total albums queued for download
  completedAlbums Int       @default(0) // Albums successfully downloaded
  failedAlbums    Int       @default(0) // Albums that failed to download
  finalSongCount  Int       @default(0) // Final number of songs in playlist
  logs            Json? // Structured logs for debugging [{timestamp, level, message}]
  errorMessage    String? // Summary error message if failed
  createdAt       DateTime  @default(now())
  completedAt     DateTime?

  jobs DownloadJob[]

  @@index([userId, weekStart])
  @@index([status])
  @@index([createdAt])
}

// ============================================
// API Keys for Mobile/External Authentication
// ============================================

model ApiKey {
  id        String   @id @default(cuid())
  userId    String
  key       String   @unique // 64-character hex string
  name      String // Device name: "iPhone 14", "Android Tablet"
  lastUsed  DateTime @default(now())
  createdAt DateTime @default(now())

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

  @@index([key])
  @@index([userId])
  @@index([lastUsed])
}

// Temporary device link codes for QR login
model DeviceLinkCode {
  id         String    @id @default(cuid())
  code       String    @unique // 6-digit alphanumeric code
  userId     String // User who generated this code
  expiresAt  DateTime // 5 minutes from creation
  usedAt     DateTime? // When the code was used
  deviceName String? // Name of device that used the code
  apiKeyId   String? // The API key created when code was used
  createdAt  DateTime  @default(now())

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

  @@index([code, expiresAt])
  @@index([userId])
}

// ============================================
// Mood Mixer System (Pre-computed Mood Buckets)
// ============================================

// Pre-computed mood assignments for fast mood mix generation
// Tracks are assigned to mood buckets during audio analysis
model MoodBucket {
  id        String   @id @default(cuid())
  trackId   String
  mood      String   // happy, sad, chill, energetic, party, focus, melancholy, aggressive, acoustic
  score     Float    // Confidence score 0-1
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  track Track @relation(fields: [trackId], references: [id], onDelete: Cascade)

  @@unique([trackId, mood])
  @@index([mood, score(sort: Desc)])
  @@index([trackId])
}

// User's active mood mix selection
// Stores the last generated mood mix for display on the home page
model UserMoodMix {
  id          String   @id @default(cuid())
  userId      String   @unique
  mood        String   // The selected mood (happy, sad, etc.)
  trackIds    String[] // Cached track IDs for the mix
  coverUrls   String[] // Cached cover URLs for display
  generatedAt DateTime // When this mix was generated
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

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

  @@index([userId])
}

// ============================================
// Enums
// ============================================

enum DiscoverStatus {
  ACTIVE // Currently in discover folder
  LIKED // User liked, will be moved to permanent
  MOVED // Already moved to permanent library
  DELETED // Week ended, was not liked
}

enum ListenSource {
  LIBRARY // From permanent /music folder
  DISCOVERY // From /music/Discover, not yet liked
  DISCOVERY_KEPT // Was discovery, user liked it
}

enum AlbumLocation {
  LIBRARY // In /music
  DISCOVER // In /music/Discover
}

// ============================================
// Notifications System
// ============================================

model Notification {
  id        String   @id @default(cuid())
  userId    String
  type      String   // system, download_complete, playlist_ready, error, import_complete
  title     String
  message   String?
  metadata  Json?    // { playlistId, albumId, artistId, etc. }
  read      Boolean  @default(false)
  cleared   Boolean  @default(false)
  createdAt DateTime @default(now())

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

  @@index([userId, cleared])
  @@index([userId, read])
  @@index([createdAt])
}

// ============================================
// Enrichment Failure Tracking
// ============================================

model EnrichmentFailure {
  id             String    @id @default(cuid())
  entityType     String    // artist, track, audio
  entityId       String    // Artist/Track ID
  entityName     String?   // Display name
  errorMessage   String?   // Human-readable error
  errorCode      String?   // Machine-readable code
  retryCount     Int       @default(0)
  maxRetries     Int       @default(3)
  firstFailedAt  DateTime  @default(now())
  lastFailedAt   DateTime  @default(now())
  skipped        Boolean   @default(false)
  skippedAt      DateTime?
  resolved       Boolean   @default(false)
  resolvedAt     DateTime?
  metadata       Json?     // Additional context (filePath, etc.)

  @@unique([entityType, entityId])
  @@index([entityType, resolved])
  @@index([skipped])
  @@index([lastFailedAt])
}
