#!/usr/bin/env node
// Suppress Node 25 deprecation warning for shell spawn (safe: args are not user-controlled)
process.removeAllListeners('warning')
/**
 * SANDCODE CLI Entry Point
 *
 * Spawns the CLI using the best available TypeScript runtime:
 *   1. Bun (preferred — native bun:bundle support)
 *   2. tsx (fallback — with shims for bun:bundle)
 *
 * npm link installs this as the global `sandcode` command.
 */
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'fs'
import { execFileSync, spawn } from 'child_process'
import { dirname, join, resolve } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const root = resolve(__dirname, '..')

// Copy vendored @sandcode/* packages to node_modules on first run
// (replaces postinstall — avoids CWD crash on npm install -g)
const vendorDir = join(root, 'src', 'vendor')
const targetDir = join(root, 'node_modules', '@sandcode')
if (existsSync(vendorDir) && !existsSync(join(targetDir, 'sdk'))) {
  try {
    mkdirSync(targetDir, { recursive: true })
    const entries = readdirSync(vendorDir).filter((e) => e.startsWith('sandcode-'))
    for (const entry of entries) {
      const pkgName = entry.replace('sandcode-', '')
      cpSync(join(vendorDir, entry), join(targetDir, pkgName), { recursive: true, force: true })
    }
    // color-diff-napi goes to node_modules/color-diff-napi (not @sandcode/)
    const cdSrc = join(vendorDir, 'sandcode-color-diff-napi')
    if (existsSync(cdSrc)) {
      cpSync(cdSrc, join(root, 'node_modules', 'color-diff-napi'), { recursive: true, force: true })
    }
  } catch {
    /* non-fatal */
  }
}
const entry = resolve(root, 'src', 'entrypoints', 'cli.tsx')
const bunPreload = resolve(root, 'shims', 'bun-preload.ts')
const loaderScript = pathToFileURL(resolve(root, 'shims', 'loader.js')).href

function hasBinary(cmd) {
  try {
    const lookupCmd = process.platform === 'win32' ? 'where' : 'which'
    execFileSync(lookupCmd, [cmd], { stdio: 'pipe', shell: false })
    return true
  } catch {
    return false
  }
}

const isWindows = process.platform === 'win32'

// Windows console codepage: Ink emits UTF-8 box-drawing chars (│, •, etc).
// If the console codepage isn't 65001, the screen buffer stores per-codepage
// bytes — display may look fine (conhost/Terminal renders glyphs cleverly),
// but copy/paste pulls bytes through the active codepage, producing mojibake
// like Γöé / ΓÇó (CP437 interpretation of UTF-8 `│`).
//
// chcp must run with REAL console handles or SetConsoleOutputCP has no
// target. stdio:'ignore' detaches cmd.exe from the console and chcp silently
// no-ops. We inherit stdio so cmd.exe attaches to the current console, then
// redirect chcp's "Active code page: 65001" banner to nul inside the cmd
// string (parsed by cmd.exe itself, not Node).
if (isWindows) {
  try {
    execFileSync('cmd.exe', ['/d', '/c', 'chcp 65001 > nul'], {
      stdio: 'inherit',
      windowsHide: true,
    })
  } catch {
    /* non-fatal: chcp unavailable on stripped Windows images */
  }
}

/**
 * On Windows, prefer the real .exe over the .cmd shim. Reason:
 *   - Node 22+ refuses to spawn .cmd without shell:true (CVE-2024-27980).
 *   - shell:true routes through cmd.exe which re-tokenizes args, breaking
 *     anything quoted by the caller (e.g. `sandcode -p "say hi"` becomes
 *     three argv entries instead of two).
 * Resolve the .cmd shim to the .exe it points at and spawn that directly
 * with shell:false. Most npm/pnpm/yarn shims follow the pattern:
 *   "%dp0%\node_modules\<pkg>\bin\<bin>.exe" %*
 */
function resolveWindowsExe(cmdName) {
  // 1. Direct .exe lookup on PATH
  try {
    const found = execFileSync('where', [cmdName + '.exe'], {
      stdio: ['ignore', 'pipe', 'ignore'],
    })
      .toString('utf-8')
      .trim()
      .split(/\r?\n/)[0]
    if (found && existsSync(found)) return found
  } catch {
    /* fall through */
  }
  // 2. Parse the .cmd shim
  try {
    const cmdPath = execFileSync('where', [cmdName + '.cmd'], {
      stdio: ['ignore', 'pipe', 'ignore'],
    })
      .toString('utf-8')
      .trim()
      .split(/\r?\n/)[0]
    if (cmdPath && existsSync(cmdPath)) {
      const content = readFileSync(cmdPath, 'utf-8')
      const match = content.match(/"([^"]*\.exe)"/i)
      if (match) {
        let exePath = match[1].replace(/%dp0%\\?/gi, dirname(cmdPath) + '\\')
        if (existsSync(exePath)) return exePath
      }
    }
  } catch {
    /* fall through */
  }
  return null
}

const hasBun = hasBinary('bun')
const hasTsx = hasBinary('tsx')

// Resolve to real .exe on Windows so spawn can use shell:false (preserves
// arg quoting + TTY). Falls back to .cmd + shell:true only if .exe can't
// be found for the runtime we actually pick (bun preferred, tsx fallback).
const bunCmd = isWindows ? (resolveWindowsExe('bun') ?? 'bun.cmd') : 'bun'
const tsxCmd = isWindows ? (resolveWindowsExe('tsx') ?? 'tsx.cmd') : 'tsx'

let command, args

if (hasBun) {
  command = bunCmd
  args = [
    'run',
    '--preload',
    bunPreload,
    '--feature=TOKEN_BUDGET',
    '--feature=DUMP_SYSTEM_PROMPT',
    '--feature=HOOK_PROMPTS',
    '--feature=FORGE',
    entry,
    ...process.argv.slice(2),
  ]
} else if (hasTsx) {
  command = tsxCmd
  args = ['--import', loaderScript, entry, ...process.argv.slice(2)]
} else {
  // Auto-install tsx as fallback runtime
  console.error('SANDCODE: No TypeScript runtime found. Installing tsx...')
  try {
    execFileSync('npm', ['install', '-g', 'tsx'], { stdio: 'inherit', shell: true })
    command = tsxCmd
    args = ['--import', loaderScript, entry, ...process.argv.slice(2)]
  } catch {
    console.error('Failed to install tsx automatically.')
    console.error('Please install manually:')
    console.error('  npm install -g tsx')
    console.error('  or: https://bun.sh')
    process.exit(1)
  }
}

// needsShell is computed AFTER `command` is selected: only the runtime we
// actually spawn matters. Computing it from both candidates falsely
// triggered shell:true whenever either fallback was a .cmd (and shell:true
// routed through cmd.exe — which is what was eating TTY inheritance even
// when we'd resolved bun.exe correctly).
const needsShell = isWindows && command.endsWith('.cmd')

// SANDCODE_DEBUG_LAUNCH=1 prints the resolved command + child exit status
// to stderr. Useful when the CLI exits silently and you don't know whether
// the fault is in the launcher or the entrypoint.
const debugLaunch = process.env.SANDCODE_DEBUG_LAUNCH === '1'
if (debugLaunch) {
  console.error(`[launcher] platform=${process.platform} node=${process.version}`)
  console.error(`[launcher] cmd=${command}`)
  console.error(`[launcher] shell=${needsShell}`)
  console.error(`[launcher] parent stdout.isTTY=${process.stdout.isTTY}`)
  console.error(`[launcher] parent stdin.isTTY=${process.stdin.isTTY}`)
  console.error(`[launcher] argv=${JSON.stringify(process.argv.slice(2))}`)
}

const child = spawn(command, args, {
  stdio: 'inherit',
  env: { ...process.env, NODE_NO_WARNINGS: '1' },
  // Only enable shell mode if we couldn't resolve to a real .exe — shell:true
  // routes through cmd.exe which re-tokenizes args (breaks quoted strings
  // like `sandcode -p "say hi"`). resolveWindowsExe avoids this for the
  // common npm/bun/tsx install layouts.
  shell: needsShell,
})

child.on('error', (err) => {
  console.error(`Error launching SANDCODE: ${err.message}`)
  process.exit(1)
})

child.on('exit', (code, signal) => {
  if (debugLaunch) {
    console.error(`[launcher] child exited code=${code} signal=${signal}`)
  }
  process.exit(code ?? 0)
})
