/**
 * desktop/src/prefs.ts — B3: pure prefs defaults, validation & merge.
 *
 * Preferences are persisted as JSON in the app's userData dir (see
 * settings-store.ts). Everything read back from disk is UNTRUSTED (a user could
 * hand-edit it, or an older/newer app version wrote a different shape), so
 * validatePrefs re-derives a fully-formed DesktopPrefs field-by-field, falling
 * back to the default whenever a field is the wrong type or out of range — it
 * never throws. Validation is hand-rolled (no zod), matching src/config.ts.
 *
 * All functions are pure and return NEW objects (immutability — coding-style
 * CRITICAL rule); they never mutate their arguments.
 */

import type { DesktopPrefs, RemoteHost } from './types.js'
import { normalizeRemoteUrl } from './remote-hosts.js'

/** Valid TCP port range (inclusive); mirrors src/config.ts's PORT bounds. */
const MIN_PORT = 1
const MAX_PORT = 65535

/**
 * The baseline preferences for a fresh install. LAN sharing is OFF by default —
 * this app hands a full shell to anyone who can reach the port, so binding to
 * 127.0.0.1 (private) is the safe default; LAN exposure is an explicit opt-in.
 *
 * `platform` is accepted for future per-platform defaults; today's defaults are
 * platform-independent, but keeping the parameter avoids a signature change
 * later (callers already thread the platform through).
 */
export function defaultPrefs(platform: NodeJS.Platform): DesktopPrefs {
  void platform // reserved for future per-platform defaults (see doc-comment)
  return {
    port: null,
    lanSharing: false,
    shellPath: null,
    openAtLogin: false,
    notifyOnApproval: true,
    notifyOnStatusChange: true,
    remoteHosts: [],
    selectedHostId: null,
  }
}

/** True iff `value` is a plain object (not null, not an array) we can index. */
function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

/** Adopt a boolean field, else fall back — anything non-boolean is rejected. */
function pickBoolean(value: unknown, fallback: boolean): boolean {
  return typeof value === 'boolean' ? value : fallback
}

/** Adopt an integer port in [MIN_PORT, MAX_PORT] or an explicit null, else fall back. */
function pickPort(value: unknown, fallback: number | null): number | null {
  if (value === null) return null
  if (
    typeof value === 'number' &&
    Number.isInteger(value) &&
    value >= MIN_PORT &&
    value <= MAX_PORT
  ) {
    return value
  }
  return fallback
}

/** Adopt a non-empty (non-whitespace) string path or an explicit null, else fall back. */
function pickShellPath(value: unknown, fallback: string | null): string | null {
  if (value === null) return null
  if (typeof value === 'string' && value.trim() !== '') return value
  return fallback
}

/**
 * Validate one untrusted remote-host record. Requires string id/name and a URL
 * that survives normalization (https + tunnel zone); the stored URL is replaced
 * with its canonical form. Returns null for anything malformed, so a corrupt or
 * hand-edited entry is dropped rather than trusted downstream.
 */
function pickRemoteHost(value: unknown): RemoteHost | null {
  if (!isRecord(value)) return null
  const { id, name, url } = value
  if (typeof id !== 'string' || id === '') return null
  if (typeof name !== 'string' || name.trim() === '') return null
  if (typeof url !== 'string') return null
  const normalizedUrl = normalizeRemoteUrl(url)
  if (normalizedUrl === null) return null
  return { id, name: name.trim(), url: normalizedUrl }
}

/**
 * Adopt a list of remote hosts from untrusted input: keep only records that
 * validate, and drop duplicate ids (first wins) so lookups are unambiguous. A
 * non-array yields an empty list.
 */
function pickRemoteHosts(value: unknown): readonly RemoteHost[] {
  if (!Array.isArray(value)) return []
  const seen = new Set<string>()
  const hosts: RemoteHost[] = []
  for (const entry of value) {
    const host = pickRemoteHost(entry)
    if (host === null || seen.has(host.id)) continue
    seen.add(host.id)
    hosts.push(host)
  }
  return hosts
}

/**
 * Adopt the selected host id: null (local), or a string that refers to a host
 * actually present in the validated list. A dangling id degrades to null so we
 * never point selection at a host that was dropped or never existed.
 */
function pickSelectedHostId(value: unknown, hosts: readonly RemoteHost[]): string | null {
  if (typeof value !== 'string') return null
  return hosts.some((host) => host.id === value) ? value : null
}

/**
 * Re-derive a fully-formed DesktopPrefs from untrusted parsed-JSON input.
 * Starts from defaults and adopts each field only when it is the right type and
 * valid; a non-object `raw` (or any bad field) yields the defaults. Never throws.
 */
export function validatePrefs(raw: unknown, platform: NodeJS.Platform): DesktopPrefs {
  const defaults = defaultPrefs(platform)
  if (!isRecord(raw)) return defaults
  const remoteHosts = pickRemoteHosts(raw['remoteHosts'])
  return {
    port: pickPort(raw['port'], defaults.port),
    lanSharing: pickBoolean(raw['lanSharing'], defaults.lanSharing),
    shellPath: pickShellPath(raw['shellPath'], defaults.shellPath),
    openAtLogin: pickBoolean(raw['openAtLogin'], defaults.openAtLogin),
    notifyOnApproval: pickBoolean(raw['notifyOnApproval'], defaults.notifyOnApproval),
    notifyOnStatusChange: pickBoolean(raw['notifyOnStatusChange'], defaults.notifyOnStatusChange),
    remoteHosts,
    selectedHostId: pickSelectedHostId(raw['selectedHostId'], remoteHosts),
  }
}

/**
 * Return a shallow copy of `patch` with `undefined`-valued fields removed, so a
 * partial update never clobbers an existing pref with `undefined` (e.g. a form
 * that only sends the fields it changed).
 */
function definedFieldsOf(patch: Partial<DesktopPrefs>): Partial<DesktopPrefs> {
  const entries = Object.entries(patch).filter(([, value]) => value !== undefined)
  return Object.fromEntries(entries) as Partial<DesktopPrefs>
}

/** Immutably merge a partial patch over a base, ignoring undefined patch fields. */
export function mergePrefs(base: DesktopPrefs, patch: Partial<DesktopPrefs>): DesktopPrefs {
  return { ...base, ...definedFieldsOf(patch) }
}
