#!/usr/bin/env node

/**
 * OpenClaude — Claude Code with any LLM
 *
 * If dist/cli.mjs exists (built), run that.
 * Otherwise, tell the user to build first or use `bun run dev`.
 */

import { existsSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
import { spawnSync } from 'child_process'
import { enableNodeCompileCacheIfAvailable } from './node-compile-cache.mjs'

const __dirname = dirname(fileURLToPath(import.meta.url))
const distPath = join(__dirname, '..', 'dist', 'cli.mjs')

const HEAP_RELAUNCHED_ENV = 'OPENCLAUDE_HEAP_RELAUNCHED'
const DISABLE_HEAP_RELAUNCH_ENV = 'OPENCLAUDE_DISABLE_HEAP_RELAUNCH'
const HEAP_SIZE_ENV = 'OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_MB'
const DEFAULT_HEAP_SIZE_MB = 8192

function hasNodeFlag(args, flag) {
  return args.some(arg => arg === flag || arg.startsWith(`${flag}=`))
}

function hasNodeOptionFlag(flag) {
  return hasNodeFlag([
    ...process.execArgv,
    ...(process.env.NODE_OPTIONS || '').split(/\s+/).filter(Boolean),
  ], flag)
}

function getHeapSizeMb() {
  // --max-memory flag overrides env var
  const maxMemArg = process.argv.find(a => a.startsWith('--max-memory='))
  if (maxMemArg) {
    const mb = Number.parseInt(maxMemArg.split('=')[1] || '0', 10)
    if (Number.isSafeInteger(mb) && mb > 0) {
      process.env[HEAP_SIZE_ENV] = String(mb)
      process.env.OPENCLAUDE_MAX_MEMORY_MB = String(mb)
      return mb
    }
  }

  const raw = process.env[HEAP_SIZE_ENV]
  if (!raw) return DEFAULT_HEAP_SIZE_MB
  const parsed = Number.parseInt(raw, 10)
  return Number.isSafeInteger(parsed) && parsed > 0
    ? parsed
    : DEFAULT_HEAP_SIZE_MB
}

function relaunchWithLongSessionHeapIfNeeded() {
  if (process.env[DISABLE_HEAP_RELAUNCH_ENV] === '1') return
  if (process.env[HEAP_RELAUNCHED_ENV] === '1') return
  const hasHeapLimit = hasNodeOptionFlag('--max-old-space-size')
  const hasExplicitGc = hasNodeOptionFlag('--expose-gc')
  if (hasHeapLimit && hasExplicitGc) return

  const execArgv = [...process.execArgv]
  if (!hasHeapLimit) {
    execArgv.push(`--max-old-space-size=${getHeapSizeMb()}`)
  }

  // Expose explicit GC for long interactive sessions. NODE_OPTIONS cannot
  // carry --expose-gc, so the executable wrapper must add it before startup.
  if (!hasExplicitGc) {
    execArgv.push('--expose-gc')
  }

  // Strip --max-memory flag before relaunching — it's a launcher-only arg
  // that Commander in the built CLI would reject as unknown.
  const childArgs = process.argv.slice(2).filter(
    arg => !arg.startsWith('--max-memory=') && arg !== '--max-memory',
  )

  // Preserve the original argv[1] (which may be a symlink like
  // /usr/local/bin/openclaude) instead of resolving it via import.meta.url.
  // Resolving symlinks here defeats install-type detection downstream: a real
  // npm global install (symlink → node_modules/@gitlawb/openclaude/bin) would
  // resolve to the package's real path inside node_modules, which is fine, but
  // a `npm install -g .` dev symlink resolves back to the repo and looks like
  // a source-tree dev run. Using argv[1] keeps the invocation path stable so
  // doctorDiagnostic's npm-global path markers can match correctly.
  const launcherPath = process.argv[1] || fileURLToPath(import.meta.url)

  const result = spawnSync(process.execPath, [
    ...execArgv,
    launcherPath,
    ...childArgs,
  ], {
    stdio: 'inherit',
    env: {
      ...process.env,
      [HEAP_RELAUNCHED_ENV]: '1',
    },
  })

  if (result.error) {
    console.error(`openclaude: failed to restart with long-session heap: ${result.error.message}`)
    process.exit(1)
  }

  process.exit(result.status ?? 1)
}

if (existsSync(distPath)) {
  relaunchWithLongSessionHeapIfNeeded()
  enableNodeCompileCacheIfAvailable()
  await import(pathToFileURL(distPath).href)
} else {
  console.error(`
  openclaude: dist/cli.mjs not found.

  Build first:
    bun run build

  Or run directly with Bun:
    bun run dev

  See README.md for setup instructions.
`)
  process.exit(1)
}
