import { app } from 'electron'
import { join } from 'path'
import { readFileSync, writeFileSync, renameSync, existsSync, copyFileSync, unlinkSync } from 'fs'
import { logger } from './logger'

/**
 * One shared persistence layer for the hand-rolled JSON array stores (history,
 * error log, templates, sources, media-items). Replaces the per-module
 * read/parse/write copies (SIMP1) and fixes three data-safety blockers:
 *
 *  - R1 -- atomic writes: write a temp file then `rename` over the target, so a
 *    crash / power-cut mid-write can never truncate the real file. (`electron-store`,
 *    used for settings, is already atomic; these stores were not.)
 *  - R2 -- corruption is no longer silent data loss: on a parse error the bad file
 *    is copied aside (`<file>.corrupt-<ts>`) before we fall back to empty, so a
 *    single bad byte can't wipe the user's history/sources from their perspective.
 *  - R3 -- an in-memory cache + debounced batched writes: the previous code
 *    re-read + re-parsed the entire (multi-MB) file and rewrote it synchronously
 *    on *every* download completion (≈O(n²) over a large channel, blocking the
 *    main thread). Reads now hit the cache; writes coalesce into one atomic flush.
 */

// Debounce window for batched writes. Long enough to coalesce a burst (e.g. many
// items enqueued/marked at once), short enough that data lands on disk promptly.
const FLUSH_DELAY_MS = 400

/**
 * Read + validate a JSON array from `path`. Invalid rows are dropped. On a parse
 * error the file is backed up to `<path>.corrupt-<timestamp>` before returning []
 * so corruption is recoverable rather than a silent wipe (R2). A missing file is
 * simply [] (first run), with no backup.
 */
export function readJsonArraySafe<T>(path: string, isValid: (o: unknown) => o is T): T[] {
  let raw: string
  try {
    if (!existsSync(path)) return []
    raw = readFileSync(path, 'utf8')
  } catch {
    return []
  }
  try {
    const data = JSON.parse(raw)
    return Array.isArray(data) ? data.filter(isValid) : []
  } catch {
    try {
      copyFileSync(path, `${path}.corrupt-${Date.now()}`)
    } catch {
      /* best-effort -- if we can't back it up, still don't crash the read */
    }
    return []
  }
}

/**
 * Atomically write `value` as JSON to `path`: serialise to `<path>.tmp`, then
 * `rename` it over the target (an atomic operation on the same volume), so a
 * crash mid-write leaves either the old file or the new one -- never a truncated
 * one (R1). `pretty` indents for human-readable files; pass false for large
 * machine-only stores to avoid inflating size/write time (R8). Returns whether the
 * write landed: a failure (disk full, read-only data dir) is logged rather than
 * swallowed silently, so a record that looked saved but vanished on restart is at
 * least diagnosable (R6). The leftover temp file is removed on failure so a partial
 * `.tmp` can't accumulate.
 */
export function writeJsonAtomic(path: string, value: unknown, pretty = true): boolean {
  const tmp = `${path}.tmp`
  try {
    writeFileSync(tmp, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value))
    renameSync(tmp, path)
    return true
  } catch (e) {
    // R6: previously a silent catch — a failed persist (disk full, read-only
    // profile) dropped the change with no trace. It now lands in the file-backed
    // diagnostic log (CC8); the user-facing toast ties to the global status surface (UI25/UX9).
    logger.error(`failed to write ${path}`, e)
    try {
      if (existsSync(tmp)) unlinkSync(tmp)
    } catch {
      /* best-effort temp cleanup */
    }
    return false
  }
}

export interface JsonStore<T> {
  /** All rows, validated; cached after the first read. */
  read(): T[]
  /** Replace all rows (capped), update the cache, and schedule an atomic flush. Returns the stored rows. */
  write(items: T[]): T[]
  /** Force any pending debounced write to disk synchronously (e.g. on quit). */
  flush(): void
}

// Every store registers itself so app-quit can flush pending writes in one call.
const registry: JsonStore<unknown>[] = []

/**
 * Create a cached, atomic JSON array store backed by `<userData>/<filename>`.
 * `cap` bounds the row count (pass Infinity for none). The path is resolved
 * lazily so importing this module never touches `app` before it's ready.
 */
export function createJsonStore<T>(
  filename: string,
  isValid: (o: unknown) => o is T,
  cap: number,
  // Pretty-print by default (small, hand-inspectable files); pass false for a
  // large machine-only store to keep writes compact (R8).
  pretty = true
): JsonStore<T> {
  let cache: T[] | null = null
  let timer: ReturnType<typeof setTimeout> | null = null
  let dirty = false

  const filePath = (): string => join(app.getPath('userData'), filename)

  function read(): T[] {
    if (cache === null) cache = readJsonArraySafe(filePath(), isValid)
    return cache
  }

  function flush(): void {
    if (timer) {
      clearTimeout(timer)
      timer = null
    }
    if (!dirty) return
    // Only clear the dirty flag once the write actually lands. A failed flush
    // (disk full, transient lock) stays dirty so the next write() or the
    // quit-time flushAllStores() retries it instead of silently dropping it (R6).
    if (writeJsonAtomic(filePath(), cache ?? [], pretty)) dirty = false
  }

  function write(items: T[]): T[] {
    // Avoid an extra copy in the common under-cap case so a per-completion write
    // stays O(1) rather than re-slicing the whole list each time (R3).
    cache = items.length > cap ? items.slice(0, cap) : items
    dirty = true
    if (timer) clearTimeout(timer)
    timer = setTimeout(flush, FLUSH_DELAY_MS)
    return cache
  }

  const store: JsonStore<T> = { read, write, flush }
  registry.push(store as JsonStore<unknown>)
  return store
}

/** Synchronously flush every store's pending write -- call on app quit. */
export function flushAllStores(): void {
  for (const s of registry) s.flush()
}
