/**
 * Pure, dependency-free validators for untrusted input that crosses a trust
 * boundary (renderer writes, backup files, hand-edited JSON on disk).
 *
 * Like buildArgs.ts, this module imports nothing from electron/node beyond the
 * `path` builtin, so it can be unit-tested without spinning up Electron.
 */

import { isAbsolute } from 'path'
import type {
  HistoryEntry,
  ErrorLogEntry,
  CommandTemplate,
  MediaProfile,
  Source,
  MediaItem
} from '@shared/ipc'

// --- Path-traversal sanitization (audit S4) ---------------------------------

/**
 * A filenameTemplate is joined onto the output directory and handed to yt-dlp's
 * `-o`. Reject anything that could write outside that directory so a malicious
 * backup/settings write can't traverse out of the chosen folder (e.g.
 * '%(title)s\..\..\win32.exe'). Legitimate templates still contain yt-dlp
 * `%(field)s` tokens and `/` or `\` for sub-folders, which are fine.
 *
 * Two Windows-specific bypasses are guarded beyond the obvious cases (audit T1):
 *  - drive-relative prefixes like 'C:foo' — `path.isAbsolute` returns FALSE for
 *    these, yet they escape the output dir, so a leading drive letter is rejected.
 *  - a '..' segment dressed up with trailing dots/spaces ('.. ', '.. .') —
 *    Windows silently trims those, so an exact `=== '..'` check would miss them.
 */
const TRAVERSAL_SEGMENT = /^[. ]*\.\.[. ]*$/
export function isSafeFilenameTemplate(template: string): boolean {
  if (isAbsolute(template) || /^[a-zA-Z]:/.test(template)) return false
  return !template.split(/[\\/]/).some((segment) => TRAVERSAL_SEGMENT.test(segment))
}

/** An output directory must be an absolute path ('' resolves to OS Downloads). */
export function isSafeOutputDir(dir: string): boolean {
  return dir === '' || isAbsolute(dir)
}

// --- Persisted-JSON entry validation (audit S5) -----------------------------

const isOptionalString = (v: unknown): boolean => v === undefined || typeof v === 'string'

/** A persisted history.json row must have the right shape or it's dropped on read. */
export function isValidHistoryEntry(o: unknown): o is HistoryEntry {
  if (!o || typeof o !== 'object') return false
  const e = o as Record<string, unknown>
  return (
    typeof e.id === 'string' &&
    typeof e.title === 'string' &&
    typeof e.url === 'string' &&
    (e.kind === 'video' || e.kind === 'audio') &&
    typeof e.quality === 'string' &&
    typeof e.completedAt === 'number' &&
    isOptionalString(e.filePath) &&
    isOptionalString(e.channel) &&
    isOptionalString(e.sizeLabel) &&
    isOptionalString(e.thumbnail)
  )
}

/** A persisted errorlog.json row must have the right shape or it's dropped on read. */
export function isValidErrorLogEntry(o: unknown): o is ErrorLogEntry {
  if (!o || typeof o !== 'object') return false
  const e = o as Record<string, unknown>
  return (
    typeof e.id === 'string' &&
    typeof e.url === 'string' &&
    (e.kind === 'video' || e.kind === 'audio') &&
    typeof e.error === 'string' &&
    typeof e.occurredAt === 'number' &&
    isOptionalString(e.title) &&
    isOptionalString(e.detail)
  )
}

/**
 * A persisted templates.json row must at least be an object carrying an id we
 * can stringify; name/args are then coerced by templates.ts's sanitize(), so a
 * weak shape check here plus normalisation there is enough.
 */
export function isTemplateLike(o: unknown): o is CommandTemplate {
  if (!o || typeof o !== 'object') return false
  const t = o as Record<string, unknown>
  return typeof t.id === 'string' || typeof t.id === 'number'
}

/**
 * A persisted profiles.json row must at least be an object carrying an id; the
 * rest is coerced by profiles.ts's sanitize(), so a weak shape check plus
 * normalisation there is enough (same approach as isTemplateLike).
 */
export function isProfileLike(o: unknown): o is MediaProfile {
  if (!o || typeof o !== 'object') return false
  const p = o as Record<string, unknown>
  return typeof p.id === 'string' || typeof p.id === 'number'
}

/** A persisted sources.json row must have the right shape or it's dropped on read. */
export function isValidSource(o: unknown): o is Source {
  if (!o || typeof o !== 'object') return false
  const s = o as Record<string, unknown>
  return (
    typeof s.id === 'string' &&
    typeof s.url === 'string' &&
    (s.kind === 'channel' || s.kind === 'playlist') &&
    typeof s.title === 'string' &&
    typeof s.addedAt === 'number' &&
    typeof s.itemCount === 'number' &&
    isOptionalString(s.channel) &&
    // feedUrl drives a network fetch in the sync, so validate its shape here too
    // (audit T7); the host is additionally restricted at the fetch boundary.
    isOptionalString(s.feedUrl) &&
    (s.watched === undefined || typeof s.watched === 'boolean') &&
    (s.lastIndexedAt === undefined || typeof s.lastIndexedAt === 'number')
  )
}

/** A persisted media-items.json row must have the right shape or it's dropped on read. */
export function isValidMediaItem(o: unknown): o is MediaItem {
  if (!o || typeof o !== 'object') return false
  const m = o as Record<string, unknown>
  return (
    typeof m.id === 'string' &&
    typeof m.sourceId === 'string' &&
    typeof m.videoId === 'string' &&
    typeof m.title === 'string' &&
    typeof m.url === 'string' &&
    typeof m.playlistTitle === 'string' &&
    typeof m.playlistIndex === 'number' &&
    typeof m.downloaded === 'boolean' &&
    isOptionalString(m.durationLabel) &&
    isOptionalString(m.filePath) &&
    (m.downloadedAt === undefined || typeof m.downloadedAt === 'number')
  )
}
