/**
 * Built-in yt-dlp terminal (Phase N): runs the bundled yt-dlp with raw, user-typed
 * arguments and streams stdout/stderr back to the renderer line-by-line.
 *
 * SECURITY: the executable is fixed to the managed yt-dlp (never an arbitrary exe),
 * and the whole feature is gated on Settings.enableCustomCommands — the same consent
 * flag that guards per-download extra args, because yt-dlp args can run arbitrary
 * code (`--exec`). The gate is enforced here in main, not just in the renderer UI.
 */
import { spawn, execFile, type ChildProcess } from 'child_process'
import { existsSync } from 'fs'
import { type WebContents } from 'electron'
import { getYtdlpPath, getBinDir, getSystem32Path } from './binaries'
import { getSettings } from './settings'
import { ensureManagedYtdlp } from './ytdlp'
import { isYtdlpUpdating, acquireSpawn, releaseSpawn } from './ytdlpLock'
import { parseExtraArgs } from './core/buildArgs'
import { createLineBuffer } from './lib/lineBuffer'
import { IpcChannels, type TerminalEvent, type TerminalRunResult } from '@shared/ipc'

const active = new Map<string, ChildProcess>()

function send(wc: WebContents, ev: TerminalEvent): void {
  if (!wc.isDestroyed()) wc.send(IpcChannels.terminalOutput, ev)
}

export function runTerminal(wc: WebContents, id: string, argsRaw: string): TerminalRunResult {
  if (!getSettings().enableCustomCommands) {
    return {
      ok: false,
      error: 'Enable "Run custom commands" in Settings → Custom commands to use the terminal.'
    }
  }
  ensureManagedYtdlp()
  const ytdlp = getYtdlpPath()
  if (!existsSync(ytdlp)) {
    return { ok: false, error: 'yt-dlp.exe is missing and could not be restored.' }
  }
  if (active.has(id)) return { ok: false, error: 'A command is already running.' }
  // L139: same exe-rewrite interlock as downloads — don't launch yt-dlp while a
  // self-update is replacing the binary.
  if (isYtdlpUpdating()) {
    return { ok: false, error: 'yt-dlp is updating — try again in a moment.' }
  }

  // Always pin ffmpeg so post-processing works; the rest is whatever the user typed.
  const args = ['--ffmpeg-location', getBinDir(), ...parseExtraArgs(argsRaw)]
  let child: ChildProcess
  try {
    child = spawn(ytdlp, args, { windowsHide: true })
  } catch (e) {
    return { ok: false, error: (e as Error).message }
  }
  active.set(id, child)
  acquireSpawn() // L139: live yt-dlp process — the updater must defer to it

  // Buffer each stream and emit on newline boundaries (flush the remainder on close).
  const lineBufs = {
    stdout: createLineBuffer((line) => send(wc, { type: 'output', id, line, stream: 'stdout' })),
    stderr: createLineBuffer((line) => send(wc, { type: 'output', id, line, stream: 'stderr' }))
  }
  child.stdout?.on('data', (c: Buffer) => lineBufs.stdout.push(c.toString()))
  child.stderr?.on('data', (c: Buffer) => lineBufs.stderr.push(c.toString()))

  let settled = false
  child.on('error', (err) => {
    if (settled) return
    settled = true
    active.delete(id)
    releaseSpawn()
    send(wc, { type: 'error', id, error: err.message })
  })
  child.on('close', (code) => {
    if (settled) return
    settled = true
    active.delete(id)
    releaseSpawn()
    // Flush any trailing partial lines that never hit a newline.
    lineBufs.stdout.flush()
    lineBufs.stderr.flush()
    send(wc, { type: 'done', id, code })
  })

  return { ok: true }
}

export function cancelTerminal(id: string): void {
  const child = active.get(id)
  if (!child) return
  const pid = child.pid
  if (pid != null) {
    // Tree-kill via System32 taskkill by absolute path (same hardening as download.ts).
    execFile(
      getSystem32Path('taskkill.exe'),
      ['/pid', String(pid), '/T', '/F'],
      { windowsHide: true },
      () => {}
    )
  } else {
    child.kill()
  }
}
