#!/usr/bin/env bun
/**
 * Build the Kilo VS Code extension and launch it in a development host.
 *
 * Usage:
 *   bun script/launch.ts [options]
 *
 * Options:
 *   --no-build        Skip the build step (reuse last build)
 *   --workspace PATH  Folder to open in VS Code (default: repo root)
 *   --mode dev|vsix   "dev" uses --extensionDevelopmentPath, "vsix" packages a VSIX (default: dev)
 *   --app-path PATH   Explicit path to the VS Code executable (auto-detected if omitted)
 *   --insiders        Prefer VS Code Insiders over stable
 *   --wait            Block until the VS Code window is closed
 *   --clean           Wipe the user-data and extensions dirs before launching
 *   --preserve-settings  Merge defaults into existing VS Code user settings
 *   --accessible      Enable VS Code accessibility support for assistive-technology testing
 *
 * Environment:
 *   VSCODE_EXEC_PATH  Path to VS Code executable (same as --app-path)
 *
 * Cross-platform: macOS, Linux, and Windows are all supported.
 *
 * The script uses a stable directory per repo checkout under the OS temp dir
 * so nothing accumulates — the same dirs are reused on every launch.
 */
import { $ } from "bun"
import { createHash } from "node:crypto"
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
import { homedir, tmpdir } from "node:os"
import { delimiter, join, resolve } from "node:path"
import { spawn } from "node:child_process"

const win = process.platform === "win32"
const root = join(import.meta.dir, "..")
const repo = resolve(root, "..", "..")

// Stable per-repo directory under OS temp — no accumulation
const hash = createHash("sha256").update(repo).digest("hex").slice(0, 12)
const base = join(tmpdir(), `kilo-vscode-dev-${hash}`)
const userDir = join(base, "user-data")
const extDir = join(base, "extensions")

// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------

function parse(argv: string[]) {
  const result: Record<string, string | boolean> = {}

  for (let i = 0; i < argv.length; i++) {
    const item = argv[i]!
    if (!item.startsWith("--")) continue

    if (item.startsWith("--no-")) {
      result[item.slice(5)] = false
      continue
    }

    const parts = item.slice(2).split("=", 2)
    const key = parts[0]!
    const raw = parts[1]
    if (raw !== undefined) {
      result[key] = raw
      continue
    }

    const next = argv[i + 1]
    if (!next || next.startsWith("--")) {
      result[key] = true
      continue
    }

    result[key] = next
    i++
  }

  return result
}

const opts = parse(process.argv.slice(2))
const shouldBuild = opts["build"] !== false
const mode = (opts["mode"] as string) ?? "dev"
const workspace = opts["workspace"] ? resolve(opts["workspace"] as string) : repo
const insiders = opts["insiders"] === true
const explicit = opts["app-path"] as string | undefined
const blocking = opts["wait"] === true
const clean = opts["clean"] === true
const preserve = opts["preserve-settings"] === true
const accessible = opts["accessible"] === true

// ---------------------------------------------------------------------------
// VS Code executable detection
// ---------------------------------------------------------------------------

function which(name: string): string | null {
  const paths = (process.env.PATH ?? "").split(delimiter).filter(Boolean)
  const exts = win ? [".cmd", ".exe", ".bat", ""] : [""]

  for (const dir of paths) {
    for (const ext of exts) {
      const full = join(dir, name.endsWith(ext) ? name : `${name}${ext}`)
      if (existsSync(full)) return full
    }
  }

  return null
}

function detect(): string {
  const env = explicit ?? process.env["VSCODE_EXEC_PATH"]
  if (env && existsSync(env)) return env

  const candidates: string[] = []
  const prefer = insiders ? "insiders" : "stable"

  if (process.platform === "darwin") {
    const order =
      prefer === "insiders"
        ? [
            "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders",
            "/Applications/Visual Studio Code.app/Contents/MacOS/Code",
          ]
        : [
            "/Applications/Visual Studio Code.app/Contents/MacOS/Code",
            "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders",
          ]
    candidates.push(...order)
  }

  if (process.platform === "linux") {
    const stable = [
      "/usr/share/code/code",
      "/usr/bin/code",
      "/snap/code/current/usr/share/code/code",
      "/var/lib/flatpak/exports/bin/com.visualstudio.code",
    ]
    const ins = [
      "/usr/share/code-insiders/code-insiders",
      "/usr/bin/code-insiders",
      "/snap/code-insiders/current/usr/share/code-insiders/code-insiders",
      "/var/lib/flatpak/exports/bin/com.visualstudio.code.insiders",
    ]
    candidates.push(...(prefer === "insiders" ? [...ins, ...stable] : [...stable, ...ins]))
  }

  if (win) {
    const local = process.env["LOCALAPPDATA"] ?? ""
    const program = process.env["PROGRAMFILES"] ?? "C:\\Program Files"

    const stable = [
      join(local, "Programs", "Microsoft VS Code", "Code.exe"),
      join(program, "Microsoft VS Code", "Code.exe"),
    ]
    const ins = [
      join(local, "Programs", "Microsoft VS Code Insiders", "Code - Insiders.exe"),
      join(program, "Microsoft VS Code Insiders", "Code - Insiders.exe"),
    ]
    candidates.push(...(prefer === "insiders" ? [...ins, ...stable] : [...stable, ...ins]))
  }

  const found = candidates.find((c) => existsSync(c))
  if (found) return found

  // Last resort: PATH lookup
  const path = insiders ? (which("code-insiders") ?? which("code")) : (which("code") ?? which("code-insiders"))
  if (path) return path

  console.error(
    `Could not find VS Code. Set VSCODE_EXEC_PATH or pass --app-path.\n` +
      `Searched:\n${candidates.map((c) => `  ${c}`).join("\n")}`,
  )
  process.exit(1)
}

// ---------------------------------------------------------------------------
// CLI path detection (needed for --mode vsix)
// ---------------------------------------------------------------------------

function codeCli(app: string): string {
  const name = app.toLowerCase().includes("insiders") ? "code-insiders" : "code"
  const direct = which(name)
  if (direct) return direct

  if (process.platform === "darwin") {
    const bundled = app.includes("Insiders")
      ? "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code"
      : "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"
    if (existsSync(bundled)) return bundled
  }

  console.error(`VS Code CLI (${name}) not found on PATH. Install the shell command or use --mode dev instead.`)
  process.exit(1)
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function newest(paths: string[]) {
  return [...paths].sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
}

// ---------------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------------

async function compile() {
  if (!shouldBuild) {
    console.log("[launch] Skipping build (--no-build)")
    return
  }

  console.log("[launch] Building extension...")
  await $`bun run package`.cwd(root).env(cleanEnv(process.env))
  console.log("[launch] Build complete")
}

function cleanEnv(input: NodeJS.ProcessEnv) {
  const env = { ...input, HOME: homedir().trim() }
  for (const key of ["XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME", "KILO_TEST_HOME"]) {
    const value = env[key]
    if (value !== undefined) env[key] = value.trim()
  }
  return env
}

// ---------------------------------------------------------------------------
// VSIX packaging (only for --mode vsix)
// ---------------------------------------------------------------------------

async function packageVsix(out: string): Promise<string> {
  await $`bunx vsce package --no-dependencies --skip-license -o ${out}/`.cwd(root)
  const files = newest(
    readdirSync(out)
      .filter((f) => f.endsWith(".vsix"))
      .map((f) => join(out, f)),
  )
  const vsix = files.at(0)
  if (!vsix) {
    console.error(`No VSIX was created in ${out}`)
    process.exit(1)
  }
  return vsix
}

async function installVsix(path: string, app: string) {
  const cmd = codeCli(app)
  await $`${cmd} --extensions-dir ${extDir} --user-data-dir ${userDir} --install-extension ${path} --force`.cwd(root)
}

// ---------------------------------------------------------------------------
// Settings for isolated instance
// ---------------------------------------------------------------------------

function settings(keep: boolean, enabled: boolean) {
  const dir = join(userDir, "User")
  const file = join(dir, "settings.json")
  const defaults = {
    "chat.disableAIFeatures": true,
    "editor.accessibilitySupport": enabled ? "on" : "off",
    "extensions.autoCheckUpdates": false,
    "extensions.autoUpdate": false,
    "extensions.ignoreRecommendations": true,
    "security.workspace.trust.enabled": false,
    "task.allowAutomaticTasks": "off",
    "telemetry.telemetryLevel": "off",
    "update.mode": "none",
    "workbench.startupEditor": "none",
    "workbench.tips.enabled": false,
    "window.commandCenter": false,
  }

  mkdirSync(dir, { recursive: true })
  const cfg =
    keep && existsSync(file)
      ? { ...defaults, ...load(file), ...(enabled ? { "editor.accessibilitySupport": "on" } : {}) }
      : defaults

  writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n")
}

function load(file: string) {
  try {
    const cfg = JSON.parse(readFileSync(file, "utf8"))
    if (cfg && typeof cfg === "object" && !Array.isArray(cfg)) return cfg as Record<string, unknown>
  } catch (err) {
    console.warn(
      `[launch] Could not parse existing settings.json, rewriting defaults: ${err instanceof Error ? err.message : String(err)}`,
    )
    return {}
  }

  console.warn("[launch] Existing settings.json root is not an object, rewriting defaults")
  return {}
}

// ---------------------------------------------------------------------------
// Launch
// ---------------------------------------------------------------------------

async function launch() {
  await compile()

  if (clean) {
    console.log("[launch] Cleaning previous state...")
    rmSync(base, { recursive: true, force: true })
  }

  mkdirSync(userDir, { recursive: true })
  mkdirSync(extDir, { recursive: true })

  const app = detect()

  settings(preserve, accessible)

  const args = [workspace, `--extensions-dir=${extDir}`, `--user-data-dir=${userDir}`, "--skip-release-notes"]

  if (mode === "dev") {
    args.push(`--extensionDevelopmentPath=${root}`)
    args.push("--disable-extension=kilocode.kilo-code")
  }

  if (mode === "vsix") {
    const out = join(base, "vsix")
    mkdirSync(out, { recursive: true })
    const vsix = await packageVsix(out)
    await installVsix(vsix, app)
    console.log(`[launch] Installed VSIX: ${vsix}`)
  }

  if (blocking) {
    args.push("--wait")
  }

  // Strip Electron/VS Code env vars so the spawned instance doesn't attach
  // to the current Electron process (e.g. when launched from a VS Code task).
  const env = cleanEnv(process.env)
  for (const key of Object.keys(env)) {
    if (key.startsWith("ELECTRON_") || key.startsWith("VSCODE_")) delete env[key]
  }

  console.log(`[launch] Starting VS Code (${mode} mode)`)
  console.log(`[launch] Executable: ${app}`)
  console.log(`[launch] Workspace:  ${workspace}`)
  console.log(`[launch] State:      ${base}`)
  console.log(`[launch] Accessibility support: ${accessible ? "on" : "off"}`)

  if (blocking) {
    const result = Bun.spawnSync([app, ...args], {
      cwd: workspace,
      env,
      stdio: ["ignore", "inherit", "inherit"],
    })
    console.log(`[launch] VS Code exited (code ${result.exitCode})`)
    return
  }

  const child = spawn(app, args, {
    cwd: workspace,
    detached: !win,
    env,
    stdio: "ignore",
    ...(win ? { shell: true } : {}),
  })
  child.unref()

  console.log(`[launch] VS Code launched (pid ${child.pid})`)
}

try {
  await launch()
} catch (err) {
  console.error(`[launch] ERROR: ${err instanceof Error ? err.message : String(err)}`)
  process.exit(1)
}
