/**
 * Persistence for the media-manager index (Pinchflat-style). Two plain-JSON
 * stores in userData, mirroring the history.ts pattern (and the same deliberate
 * choice to stay on JSON rather than better-sqlite3 -- revisit if a user indexes
 * many large channels):
 *
 *   sources.json      -- one Source record per added channel/playlist
 *   media-items.json  -- every MediaItem across all sources (queried by sourceId)
 *
 * Per-row validation on read (validation.ts) so a hand-edited or corrupted file
 * can't feed the UI malformed records (same approach as history/errorlog).
 */

import { existsSync, unlinkSync } from 'fs'
import type { Source, MediaItem } from '@shared/ipc'
import { isValidSource, isValidMediaItem } from './core/validation'
import { mergeItemsPreservingState } from './core/indexerCore'
import { createJsonStore } from './jsonStore'
// A generous global cap (MEDIA_ITEMS_MAX, see constants.ts) so a runaway index
// can't grow the file unbounded; large enough for several big channels. When
// exceeded, the most-recently-written source's items are kept (they're placed
// first by replaceMediaItems).
import { MEDIA_ITEMS_MAX as MAX_ITEMS } from './constants'

// Two cached, atomically-written stores (R1–R3 via the shared jsonStore). The
// media-items store is the hot path: setMediaItemDownloaded used to re-read and
// rewrite the whole (≤MAX_ITEMS) file on every completion; now reads hit the
// cache and writes are batched. Sources are few, so that store is uncapped.
const sourcesStore = createJsonStore('sources.json', isValidSource, Infinity)
// Compact (not pretty) -- this store can hold up to MAX_ITEMS rows; indentation
// would needlessly inflate its size and write time (R8).
const itemsStore = createJsonStore('media-items.json', isValidMediaItem, MAX_ITEMS, false)

// --- Sources ----------------------------------------------------------------

export function listSources(): Source[] {
  return sourcesStore.read()
}

export function getSource(id: string): Source | undefined {
  return listSources().find((s) => s.id === id)
}

/** Insert or replace a source by id (a re-index updates the existing record). */
export function upsertSource(source: Source): Source[] {
  return sourcesStore.write([source, ...listSources().filter((s) => s.id !== source.id)])
}

/** Toggle whether a source is watched for new uploads (Phase J). Returns all sources. */
export function setSourceWatched(id: string, watched: boolean): Source[] {
  return sourcesStore.write(listSources().map((s) => (s.id === id ? { ...s, watched } : s)))
}

/**
 * Point a source at a MediaProfile, or clear it (profileId === null). Returns
 * all sources. A cleared profile drops the field entirely so the record stays
 * clean rather than carrying an explicit undefined.
 */
export function setSourceProfile(id: string, profileId: string | null): Source[] {
  return sourcesStore.write(
    listSources().map((s) => {
      if (s.id !== id) return s
      const { profileId: _drop, ...rest } = s
      return profileId ? { ...rest, profileId } : rest
    })
  )
}

/** Remove a source and all of its media items. Returns the remaining sources. */
export function removeSource(id: string): Source[] {
  const sources = sourcesStore.write(listSources().filter((s) => s.id !== id))
  itemsStore.write(listAllItems().filter((m) => m.sourceId !== id))
  return sources
}

// --- Media items ------------------------------------------------------------

function listAllItems(): MediaItem[] {
  return itemsStore.read()
}

export function listMediaItems(sourceId: string): MediaItem[] {
  return listAllItems().filter((m) => m.sourceId === sourceId)
}

/**
 * Replace all media items for one source with a fresh set (the result of a
 * (re)index). Other sources' items are preserved in full; only the source being
 * written here absorbs the MAX_ITEMS cap, so re-indexing one (large) source can
 * never silently evict another source's items past the global limit (M23).
 */
export function replaceMediaItems(sourceId: string, items: MediaItem[]): void {
  const others = listAllItems().filter((m) => m.sourceId !== sourceId)
  const budget = Math.max(0, MAX_ITEMS - others.length)
  itemsStore.write([...items.slice(0, budget), ...others])
}

/**
 * Incrementally merge a freshly-indexed item list into the persisted store,
 * preserving the downloaded state of videos already on disk (see Phase I). The
 * fresh list defines current membership/order; returns the merged items and how
 * many were new since the last index.
 */
export function mergeMediaItems(
  sourceId: string,
  fresh: MediaItem[]
): { items: MediaItem[]; newCount: number } {
  const result = mergeItemsPreservingState(listMediaItems(sourceId), fresh)
  replaceMediaItems(sourceId, result.items)
  return result
}

/**
 * Mark one media item downloaded, recording its file path + time. Returns the
 * updated list for that item's source (or [] if the id is unknown). Used by the
 * library view once a queued item completes (Phase H/I).
 */
export function setMediaItemDownloaded(
  id: string,
  filePath?: string,
  quality?: string
): MediaItem[] {
  const all = listAllItems()
  const target = all.find((m) => m.id === id)
  if (!target) return []
  const updated = all.map((m) =>
    m.id === id
      ? { ...m, downloaded: true, downloadedAt: Date.now(), filePath, downloadedQuality: quality }
      : m
  )
  itemsStore.write(updated)
  return updated.filter((m) => m.sourceId === target.sourceId)
}

/**
 * Prune (delete the files of) the given media items for a source, then clear
 * their downloaded state so the Library shows them as pending again. The
 * download-archive guard (Settings.useDownloadArchive) keeps a pruned video from
 * silently re-downloading on the next "Download new only". Returns the updated
 * item list for the source and how many files were actually deleted.
 *
 * File deletion is the caller's explicit, dry-run-confirmed action (the source
 * detail's Prune button) — never automatic.
 */
export function pruneMediaItems(
  sourceId: string,
  itemIds: string[]
): { items: MediaItem[]; deleted: number } {
  const remove = new Set(itemIds)
  const all = listAllItems()
  let deleted = 0
  const updated = all.map((m) => {
    if (!remove.has(m.id)) return m
    if (m.filePath && existsSync(m.filePath)) {
      try {
        unlinkSync(m.filePath)
        deleted++
      } catch {
        /* best-effort — a locked/already-gone file just stays counted as not-deleted */
      }
    }
    // Clear the on-disk state; the download archive prevents a silent re-download.
    const { filePath: _f, downloadedAt: _a, downloadedQuality: _q, ...rest } = m
    return { ...rest, downloaded: false }
  })
  itemsStore.write(updated)
  return { items: updated.filter((m) => m.sourceId === sourceId), deleted }
}
