/**
 * The one validate-AND-coerce layer for Settings writes (CC9). Every key that
 * setSettings accepts is described here as a zod schema whose output is exactly
 * what gets persisted — replacing the bespoke per-key switch. The schemas reuse
 * validation.ts's domain predicates (`isValidSyncTime`, `isSafeOutputDir`,
 * `isSafeFilenameTemplate`) as refinements, so the rules live in one place.
 *
 * Semantics are a faithful port of the old switch (unit-tested in
 * test/settingsSchema.test.ts): invalid values fail parse and are SKIPPED by
 * applySettings (the stored value stays), numbers coerce + clamp, strings trim
 * where they trimmed before. Side effects (secret encryption, launch-at-startup
 * registration) stay in settings.ts — this layer is pure.
 *
 * The jsonStore row guards (`isValid*` in validation.ts) intentionally stay
 * type-guard predicates: they filter untrusted disk rows in a hot read path and
 * need no coercion.
 */
import { z } from 'zod'
import {
  ACCENT_COLORS,
  COOKIE_BROWSERS,
  VIDEO_QUALITY_OPTIONS,
  AUDIO_QUALITY_OPTIONS,
  YTDLP_UPDATE_CHANNELS,
  isValidSyncTime,
  type Settings
} from '@shared/ipc'
import { isSafeOutputDir, isSafeFilenameTemplate } from './core/validation'
import { sanitizeOptions } from './settingsOptions'

/** Coerce to a finite number, round, and clamp into [min, max] (maxConcurrent, aria2c). */
const clampedInt = (min: number, max: number) =>
  z.coerce
    .number()
    .refine((n) => Number.isFinite(n))
    .transform((n) => Math.min(max, Math.max(min, Math.round(n))))

/** Trimmed string that must satisfy the given predicate after trimming. */
const trimmedWhere = (ok: (v: string) => boolean) =>
  z
    .string()
    .transform((v) => v.trim())
    .refine(ok)

// yt-dlp rate-limit format: empty (disabled) or e.g. "500K", "2.5M", "1G".
const RATE_LIMIT_RE = /^(\d+\.?\d*[KMGkmg]?B?)?$/

const bool = z.boolean()

/**
 * One schema per Settings key; `applySettings` looks the key up, `safeParse`s
 * the incoming value, and stores the parsed output — or skips the key entirely
 * on failure. Typed as ZodType<unknown> per-field because outputs differ.
 */
export const SETTINGS_FIELD_SCHEMAS: { [K in keyof Settings]: z.ZodType<Settings[K], unknown> } = {
  theme: z.enum(['light', 'dark', 'system']),
  accentColor: z.enum(ACCENT_COLORS),
  defaultKind: z.enum(['video', 'audio']),
  maxConcurrent: clampedInt(1, 5),
  // aria2c caps -x/-s at 16; clamp like maxConcurrent rather than reject (L64).
  aria2cConnections: clampedInt(1, 16),
  // Reaches `schtasks /ST` when the daily sync is (re)registered (L51).
  syncTime: z.string().refine(isValidSyncTime),
  watchClipboard: bool,
  useAria2c: bool,
  restrictFilenames: bool,
  useDownloadArchive: bool,
  autoUpdateYtdlp: bool,
  enableCustomCommands: bool,
  notifyOnComplete: bool,
  autoDownloadNew: bool,
  hasCompletedOnboarding: bool,
  minimizeToTray: bool,
  isSidebarCollapsed: bool,
  useHardwareAcceleration: bool,
  launchAtStartup: bool,
  // Same allowlist that guards the `--update-to` flag (audit F1).
  ytdlpChannel: z.enum(YTDLP_UPDATE_CHANNELS),
  // Set by the main-process auto-updater; a bogus value at worst skips one check.
  ytdlpLastUpdateCheck: z.coerce.number().refine((n) => Number.isFinite(n) && n >= 0),
  defaultTemplateId: z.union([z.null(), z.string()]),
  cookieSource: z.enum(['none', 'browser', 'login']),
  cookiesBrowser: z.enum(COOKIE_BROWSERS),
  // Always store a fully-validated object; the renderer sends the whole group.
  downloadOptions: z.unknown().transform((v) => sanitizeOptions(v)),
  // An empty string is allowed — it clears the override and restores the default.
  videoFolder: trimmedWhere((v) => v === '' || isSafeOutputDir(v)),
  audioFolder: trimmedWhere((v) => v === '' || isSafeOutputDir(v)),
  filenameTemplate: trimmedWhere(isSafeFilenameTemplate),
  // Collection folder layout (PINCHFLAT): empty = built-in default; otherwise a
  // safe relative yt-dlp template (isSafeFilenameTemplate allows '/'-separated
  // path segments and rejects absolute paths + '..' traversal).
  collectionOutputTemplate: trimmedWhere((v) => v === '' || isSafeFilenameTemplate(v)),
  defaultVideoQuality: z.enum(VIDEO_QUALITY_OPTIONS),
  defaultAudioQuality: z.enum(AUDIO_QUALITY_OPTIONS),
  rateLimit: trimmedWhere((v) => RATE_LIMIT_RE.test(v)),
  // Throttle seconds (0–300, 0 = off) to dodge YouTube's rate-limit/403 wall.
  sleepRequests: clampedInt(0, 300),
  sleepInterval: clampedInt(0, 300),
  maxSleepInterval: clampedInt(0, 300),
  // Power-user field; yt-dlp's client list evolves. Accept any trimmed string.
  youtubePlayerClient: z.string().transform((v) => v.trim()),
  // Credential-bearing fields — settings.ts encrypts these AFTER parsing.
  proxy: z.string(),
  youtubePoToken: z.string()
}

/**
 * Parse one incoming settings value. Returns `{ ok: true, value }` with the
 * coerced/clamped/trimmed value to store, or `{ ok: false }` when the write
 * must be skipped (unknown key or failed validation).
 */
export function parseSettingsField<K extends keyof Settings>(
  key: K,
  value: unknown
): { ok: true; value: Settings[K] } | { ok: false } {
  const schema = SETTINGS_FIELD_SCHEMAS[key] as z.ZodType<Settings[K], unknown> | undefined
  if (!schema) return { ok: false }
  const parsed = schema.safeParse(value)
  return parsed.success ? { ok: true, value: parsed.data } : { ok: false }
}
