import { app, dialog, type BrowserWindow, type OpenDialogOptions, type WebContents } from 'electron'
import { existsSync, mkdirSync, copyFileSync, renameSync, rmSync } from 'fs'
import { execFile } from 'child_process'
import { join, dirname, basename } from 'path'
import {
  getFfmpegPath,
  getFfprobePath,
  getFfmpegDir,
  getBundledFfmpegPath,
  getBundledFfprobePath,
  getSystem32Path
} from './binaries'
import { getFfmpegVersions } from './ffmpeg'
import { streamVerifiedFile } from './lib/verifiedDownload'
import { FFMPEG_ARCHIVE_URL, FFMPEG_ARCHIVE_SHA256, FFMPEG_TRUSTED_HOSTS } from './config'
import { VERSION_TIMEOUT_MS, FFMPEG_EXTRACT_TIMEOUT_MS } from './constants'
import {
  IpcChannels,
  type FfmpegSetupStatus,
  type FfmpegSetupProgress,
  type FfmpegSetupResult
} from '@shared/ipc'
import { logger } from './logger'

/**
 * First-run acquisition of ffmpeg + ffprobe. The installer no longer bundles them
 * (they're the bulk of its size); they live in the managed userData/bin dir (like the
 * self-updating yt-dlp) and are fetched once on first run from a PINNED upstream archive,
 * verified against a pinned SHA-256, then unpacked with the OS's tar.exe. See config.ts
 * for the source + trust rationale, and binaries.ts for why the managed dir is used.
 */

/**
 * Trust gate for the ffmpeg archive: HTTPS + an allowlisted upstream host, re-checked on
 * every redirect hop by streamVerifiedFile (github.com 302s to a githubusercontent host).
 * Mirrors updater.ts's isTrustedDownloadUrl but for the upstream host set, not our own.
 */
export function isTrustedFfmpegUrl(url: string): boolean {
  try {
    const u = new URL(url)
    return u.protocol === 'https:' && (FFMPEG_TRUSTED_HOSTS as readonly string[]).includes(u.host)
  } catch {
    return false
  }
}

/** Both binaries present in the managed dir — the readiness the download gate keys on. */
function bothPresent(): boolean {
  return existsSync(getFfmpegPath()) && existsSync(getFfprobePath())
}

/** Copy a dev seed into the managed dir when the managed copy is missing. */
function seedOne(seed: string, managed: string): void {
  if (existsSync(managed) || !existsSync(seed)) return
  try {
    mkdirSync(dirname(managed), { recursive: true })
    copyFileSync(seed, managed)
  } catch {
    /* best-effort; a failed seed just leaves the managed copy absent → first-run download */
  }
}

/**
 * Ensure the managed ffmpeg/ffprobe exist, seeding them from the read-only bundle when
 * present (dev checkouts keep them in resources/bin, so `npm run dev` needs no download).
 * A shipped installer carries no seed, so this no-ops and first-run download provides them.
 * Best-effort and idempotent — cheap to call at startup and before the presence gate.
 * Mirrors ensureManagedYtdlp in ytdlp.ts.
 */
export function ensureManagedFfmpeg(): void {
  seedOne(getBundledFfmpegPath(), getFfmpegPath())
  seedOne(getBundledFfprobePath(), getFfprobePath())
}

/** Readiness + versions for the setup gate and the About card. */
export async function getFfmpegSetupStatus(): Promise<FfmpegSetupStatus> {
  // Seed from the dev bundle first so a seeded checkout reports ready without a download.
  ensureManagedFfmpeg()
  const versions = await getFfmpegVersions()
  return { ready: bothPresent(), ffmpeg: versions.ffmpeg, ffprobe: versions.ffprobe }
}

// The in-flight download's canceller (armed only during the network phase; extraction is
// not cancelable) and a single-flight guard shared by download + locate.
let activeCancel: (() => void) | null = null
let extracting = false

/** Abort the in-flight ffmpeg archive download, if any (the setup card's Cancel). */
export function cancelFfmpegDownload(): void {
  activeCancel?.()
}

/**
 * Download the pinned ffmpeg archive, verify its SHA-256, unpack ffmpeg.exe + ffprobe.exe
 * into the managed dir, and clean up. Progress (download %, then an indeterminate
 * "extracting" phase) is pushed to the renderer. Single-flight; the network phase is
 * cancelable via cancelFfmpegDownload().
 */
export async function downloadFfmpeg(wc: WebContents): Promise<FfmpegSetupResult> {
  if (activeCancel || extracting) {
    return { ok: false, error: 'An ffmpeg download is already in progress.' }
  }
  if (!isTrustedFfmpegUrl(FFMPEG_ARCHIVE_URL)) {
    return { ok: false, error: 'Refused to download ffmpeg from an untrusted location.' }
  }

  const tempZip = join(app.getPath('temp'), 'aerofetch-ffmpeg.zip')
  const extractDir = join(app.getPath('temp'), `aerofetch-ffmpeg-${Date.now()}`)
  const send = (p: FfmpegSetupProgress): void => {
    if (!wc.isDestroyed()) wc.send(IpcChannels.ffmpegSetupProgress, p)
  }

  try {
    const dl = await streamVerifiedFile({
      url: FFMPEG_ARCHIVE_URL,
      filePath: tempZip,
      expectedSha: FFMPEG_ARCHIVE_SHA256,
      isTrusted: isTrustedFfmpegUrl,
      onProgress: (p) =>
        send({ phase: 'downloading', received: p.received, total: p.total, fraction: p.fraction }),
      onCancelReady: (cancel) => {
        activeCancel = cancel
      },
      onSettled: (cancel) => {
        if (activeCancel === cancel) activeCancel = null
      },
      messages: {
        checksumMismatch:
          'The ffmpeg download failed its checksum check — it may be corrupt or tampered with. Nothing was installed.',
        canceled: 'ffmpeg download canceled.'
      }
    })
    if (!dl.ok) return { ok: false, error: dl.error }

    extracting = true
    send({ phase: 'extracting' })
    await extractArchive(tempZip, extractDir)
    installFromDir(extractDir)
    if (!bothPresent()) {
      return { ok: false, error: 'The ffmpeg archive did not contain the expected binaries.' }
    }
    return { ok: true }
  } catch (e) {
    logger.error('[ffmpeg-setup] download failed', e instanceof Error ? e.message : String(e))
    return { ok: false, error: e instanceof Error ? e.message : String(e) }
  } finally {
    extracting = false
    activeCancel = null
    rmSync(tempZip, { force: true })
    rmSync(extractDir, { recursive: true, force: true })
  }
}

/**
 * Unpack just ffmpeg.exe + ffprobe.exe (flattened) from the archive using the OS's bundled
 * bsdtar. Resolve tar.exe by absolute System32 path, never the bare name, so a planted
 * tar.exe on PATH / in the CWD can't run in its place (audit F3). The two glob patterns
 * (matching the `bin/ffmpeg.exe` + `bin/ffprobe.exe` members) are applied by tar itself —
 * execFile does no shell globbing — and --strip-components=2 drops the leading
 * `<build>/bin/` path so the exes land flat in destDir.
 */
function extractArchive(zip: string, destDir: string): Promise<void> {
  mkdirSync(destDir, { recursive: true })
  return new Promise((resolve, reject) => {
    execFile(
      getSystem32Path('tar.exe'),
      ['-xf', zip, '-C', destDir, '--strip-components=2', '*/bin/ffmpeg.exe', '*/bin/ffprobe.exe'],
      { timeout: FFMPEG_EXTRACT_TIMEOUT_MS, windowsHide: true },
      (err) => (err ? reject(err) : resolve())
    )
  })
}

/** Move the two extracted exes into the managed dir. */
function installFromDir(dir: string): void {
  mkdirSync(getFfmpegDir(), { recursive: true })
  installOne(join(dir, 'ffmpeg.exe'), getFfmpegPath())
  installOne(join(dir, 'ffprobe.exe'), getFfprobePath())
}

/**
 * Place one binary at its managed path via a temp copy + rename, so a concurrent read
 * never sees a half-written exe (rename is atomic within a volume). Falls back to an
 * in-place overwrite only if the rename is blocked by a same-name destination.
 */
function installOne(src: string, dest: string): void {
  if (!existsSync(src)) throw new Error(`the ffmpeg archive is missing ${basename(dest)}`)
  const tmp = `${dest}.part`
  copyFileSync(src, tmp)
  try {
    renameSync(tmp, dest)
  } catch {
    copyFileSync(tmp, dest)
    rmSync(tmp, { force: true })
  }
}

/** Run `<tool> -version` to confirm a manually-located binary is actually runnable. */
function validateTool(path: string): Promise<boolean> {
  return new Promise((resolve) => {
    execFile(
      path,
      ['-version'],
      { timeout: VERSION_TIMEOUT_MS, windowsHide: true },
      (err, stdout) => resolve(!err && /version/i.test(String(stdout)))
    )
  })
}

/**
 * Offline / air-gapped fallback: let the user point at a folder that already holds
 * ffmpeg.exe + ffprobe.exe (or its `bin/` subfolder), validate both actually run, and
 * copy them into the managed dir. This is what keeps the hard first-run gate escapable
 * without a network connection.
 */
export async function locateFfmpegManually(win?: BrowserWindow): Promise<FfmpegSetupResult> {
  if (activeCancel || extracting) {
    return { ok: false, error: 'An ffmpeg download is already in progress.' }
  }
  const opts: OpenDialogOptions = {
    properties: ['openDirectory'],
    title: 'Select the folder containing ffmpeg.exe and ffprobe.exe'
  }
  const res = win ? await dialog.showOpenDialog(win, opts) : await dialog.showOpenDialog(opts)
  if (res.canceled || !res.filePaths[0]) return { ok: false, error: 'No folder selected.' }

  const chosen = res.filePaths[0]
  for (const cand of [chosen, join(chosen, 'bin')]) {
    const ffmpeg = join(cand, 'ffmpeg.exe')
    const ffprobe = join(cand, 'ffprobe.exe')
    if (!existsSync(ffmpeg) || !existsSync(ffprobe)) continue
    const runnable = (await validateTool(ffmpeg)) && (await validateTool(ffprobe))
    if (!runnable) {
      return {
        ok: false,
        error: 'Those binaries could not be run — they may be blocked or corrupt.'
      }
    }
    try {
      mkdirSync(getFfmpegDir(), { recursive: true })
      installOne(ffmpeg, getFfmpegPath())
      installOne(ffprobe, getFfprobePath())
    } catch (e) {
      return { ok: false, error: e instanceof Error ? e.message : String(e) }
    }
    return { ok: true }
  }
  return { ok: false, error: "That folder doesn't contain ffmpeg.exe and ffprobe.exe." }
}
