import { existsSync, mkdirSync, copyFileSync } from 'fs'
import { dirname } from 'path'
import { getYtdlpPath, getBundledYtdlpPath, YTDLP_MISSING_MSG } from './binaries'
import { execFileAsync } from './lib/exec'
import { cleanError } from './log'
import { VERSION_TIMEOUT_MS, YTDLP_UPDATE_TIMEOUT_MS } from './constants'
import { getSettings, setSettings } from './settings'
import { shouldAutoCheckYtdlp } from './core/ytdlpPolicy'
import { beginYtdlpUpdate, endYtdlpUpdate, liveSpawnCount } from './ytdlpLock'
import {
  isYtdlpUpdateChannel,
  type YtdlpVersionResult,
  type YtdlpUpdateChannel,
  type YtdlpUpdateResult,
  type YtdlpAutoUpdateStatus
} from '@shared/ipc'

/**
 * Ensure the writable managed yt-dlp.exe exists, copying the bundled seed in if
 * it doesn't (first run, or after the user/AV deleted it). Best-effort and
 * idempotent — when the copy is already present this is just one existsSync, so
 * it's cheap to call before any spawn/update. Does nothing if the seed itself is
 * gone (a broken install); callers surface their own missing-binary error then.
 */
export function ensureManagedYtdlp(): void {
  const managed = getYtdlpPath()
  if (existsSync(managed)) return
  const seed = getBundledYtdlpPath()
  if (!existsSync(seed)) return
  try {
    mkdirSync(dirname(managed), { recursive: true })
    copyFileSync(seed, managed)
  } catch {
    /* best-effort; a failed seed just leaves the managed copy absent */
  }
}

/** Spawn the bundled yt-dlp and read back its `--version` for the Settings panel. */
export async function getYtdlpVersion(): Promise<YtdlpVersionResult> {
  const ytdlpPath = getYtdlpPath()

  if (!existsSync(ytdlpPath)) {
    return { ok: false, error: YTDLP_MISSING_MSG }
  }

  const r = await execFileAsync(ytdlpPath, ['--version'], { timeout: VERSION_TIMEOUT_MS })
  if (!r.ok) {
    // Run stderr through the shared cleaner so yt-dlp's noisy output collapses to
    // its trailing error line, consistent with download/probe/indexer (CC6).
    const msg = r.timedOut
      ? 'Timed out running yt-dlp.'
      : cleanError(r.stderr) || r.error?.message || ''
    return { ok: false, error: msg }
  }
  return { ok: true, version: r.stdout.trim() }
}

/**
 * Self-update the bundled yt-dlp.exe via its built-in `--update-to <channel>`
 * (stable releases or the nightly build). Requires write access to the file
 * yt-dlp.exe lives in, which holds for both the dev resources/bin checkout and
 * a per-user install; it would fail under a locked-down system install.
 */
export async function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpdateResult> {
  // Validate against the channel allowlist BEFORE the value reaches `--update-to`.
  // That flag also accepts `OWNER/REPO@TAG`, which would download and install an
  // arbitrary binary over yt-dlp.exe — so an unrecognised value (e.g. forged by a
  // compromised renderer over IPC) must never be forwarded. (audit F1)
  if (!isYtdlpUpdateChannel(channel)) {
    return { ok: false, error: 'Unsupported update channel.' }
  }

  // Self-heal: restore the managed copy from the bundled seed before updating, so
  // a deleted yt-dlp.exe is re-seeded and then updated in one click.
  ensureManagedYtdlp()
  const ytdlpPath = getYtdlpPath()

  if (!existsSync(ytdlpPath)) {
    return { ok: false, error: YTDLP_MISSING_MSG }
  }

  // L139: `--update-to` rewrites yt-dlp.exe in place. Take the spawn interlock so a
  // download/terminal can't execute the exe mid-swap (Windows sharing violation /
  // half-written binary). If a spawn (or another update) is already live, defer
  // rather than fight it — the update is best-effort; the startup path retries next
  // launch and the Settings "Update now" button surfaces this message for a manual retry.
  if (!beginYtdlpUpdate()) {
    return {
      ok: false,
      error: 'yt-dlp is busy (a download or another update is running) — try again shortly.'
    }
  }
  try {
    const r = await execFileAsync(ytdlpPath, ['--update-to', channel], {
      timeout: YTDLP_UPDATE_TIMEOUT_MS
    })
    if (!r.ok) {
      const msg = r.timedOut
        ? 'Timed out updating yt-dlp.'
        : cleanError(r.stderr) || r.error?.message || ''
      return { ok: false, error: msg }
    }
    return { ok: true, output: r.stdout.trim() }
  } finally {
    endYtdlpUpdate()
  }
}

/**
 * Startup yt-dlp maintenance: always seed the managed copy, then — if auto-update
 * is on and the daily throttle has elapsed — self-update it to the configured
 * channel. Whether it changed is decided by comparing `--version` before/after
 * (robust against yt-dlp's localized "up to date" wording). Status is pushed to
 * the renderer so the Settings UI reflects a check it didn't trigger.
 *
 * Best-effort: every failure path still records the attempt time (so an
 * unreachable update server doesn't re-hit the network every launch) and never
 * throws — a stale binary must never block the app from starting.
 */
export async function runStartupYtdlpAutoUpdate(
  send: (status: YtdlpAutoUpdateStatus) => void
): Promise<void> {
  ensureManagedYtdlp()
  const settings = getSettings()
  if (!shouldAutoCheckYtdlp(settings.autoUpdateYtdlp, settings.ytdlpLastUpdateCheck, Date.now())) {
    return
  }
  // L139: never rewrite the exe while a download/terminal is live (e.g. an auto-
  // resumed persisted-queue item). Skip WITHOUT recording the check so the daily
  // throttle isn't burned on a deferral — retry next launch. Re-checked right before
  // the rewrite too, since the version probe below awaits.
  if (liveSpawnCount() > 0) return

  const channel = settings.ytdlpChannel
  send({ phase: 'checking', channel })

  const before = await getYtdlpVersion()
  if (liveSpawnCount() > 0) {
    // A spawn started during the version probe — defer to next launch (no record).
    send({ phase: 'current', channel, version: before.ok ? before.version : undefined })
    return
  }
  const result = await updateYtdlp(channel)
  setSettings({ ytdlpLastUpdateCheck: Date.now() })

  if (!result.ok) {
    send({ phase: 'error', channel, error: result.error, checkedAt: Date.now() })
    return
  }

  const after = await getYtdlpVersion()
  const changed = before.ok && after.ok && before.version !== after.version
  send({
    phase: changed ? 'updated' : 'current',
    channel,
    version: after.ok ? after.version : undefined,
    checkedAt: Date.now()
  })
}
