import { appendFileSync } from 'node:fs'
import {
  ensurePrefixedEnvValue,
  readPrefixedEnv,
  readPrefixedEnvFlag,
  setPrefixedEnvValue,
} from '#cli/core/env'
import { startMarkdownCacheCleanup } from '#cli/core/markdown'
import { startEventLoopLagLogger } from '#cli/core/observability/event-loop-lag'
import { startGcLogger } from '#cli/core/observability/gc-log'
import {
  isOneShotEnabled,
  isPerfEnabled,
  perfLog,
} from '#cli/core/observability/perf-log'
import { traceLog } from '#cli/core/observability/trace-log'
import { resolveInitialThemeMode } from '#cli/core/theme'
import { detectTerminalColorCapability } from '@simon_he/vue-tui'
import { watch } from 'vue'
import { getHelpText, isHelpRequested, parseCliArgs } from './cli/args'
import {
  isSelfContainedBinary,
  maybeRelaunchToUpdatedBinary,
  runBinaryUpgradeCommand,
} from './cli/binary-upgrade'
import { runExec } from './cli/exec'
import {
  createDimSdkShellProps,
  type DimSdkShellProps,
} from './cli/dimsdk-app'
import {
  queueUpgradeToast,
  requestFatalExit,
  setupCrashHandlers,
  withTimeout,
} from './cli/runtime'
import { runTerminalUi, type ThemeUpdate } from './cli/terminal'
import { resolveTopLevelSubcommand } from './cli/top-level'
import { checkUpgrade, getNpmPackageOverride } from './cli/upgrade'
import { runNpmUpgradeCommand } from './cli/upgrade-check'
import {
  createUpgradeMessageReporter,
  resolveBackgroundUpgradeCheckIntervalMs,
  startBackgroundUpgradeChecks,
} from './cli/upgrade-loop'
import { printCliVersionInfo } from './cli/version-info'
import { runAuthCommand } from './commands/auth'
import { isNextApiAuthEnabled } from './core/feature-flags'
import { createDimSdkRouter, DimSdkShell } from './index'
import {
  restoreTerminalTitle,
  saveTerminalTitle,
  setTerminalTitle,
} from './ui/terminal-title'
import { createUiThemeSwitcher, DimSdkUiThemeKey } from './ui/theme'

declare const __CLI_VERSION__: string | undefined

type RuntimeGlobals = typeof globalThis & {
  process?: NodeJS.Process
}

type MutableEnvRecord = Record<string, string | undefined>
type TerminalInstaller = NonNullable<Parameters<typeof runTerminalUi>[0]['install']>
type TerminalAppInstance = Parameters<TerminalInstaller>[0]

function runtimeGlobals(): RuntimeGlobals {
  return globalThis as RuntimeGlobals
}

const debugFlag = readPrefixedEnvFlag(undefined, 'DEBUG')
let debugEnabled =
  debugFlag === '1' ||
  debugFlag === 'true' ||
  debugFlag === 'yes' ||
  debugFlag === 'on' ||
  process.argv.includes('--debug')
const DEBUG_LOG_PATH =
  readPrefixedEnv(undefined, 'DEBUG_LOG') || './dimcode-debug.log'
// Cap upgrade checks so a slow npm registry doesn't delay CLI startup too long.
const NPM_UPGRADE_CHECK_TIMEOUT_MS = 1500
const STARTUP_UPGRADE_WAIT_ENV = 'DIMCODE_STARTUP_UPGRADE_WAIT_MS'

function logDebug(message: string, data?: unknown): void {
  if (!debugEnabled) return
  const timestamp = new Date().toISOString()
  const line = `[${timestamp}] ${message}${data ? `: ${JSON.stringify(data, null, 2)}` : ''}\n`
  try {
    appendFileSync(DEBUG_LOG_PATH, line)
  } catch {
    // Ignore write errors
  }
}

function logFatalSetupError(err: unknown): void {
  const error = err instanceof Error ? err : new Error(String(err))
  const payload = { message: error.message, stack: error.stack }
  if (debugEnabled) {
    logDebug('[CLI] fatal error during setup', payload)
    return
  }

  const timestamp = new Date().toISOString()
  const line = `[${timestamp}] [CLI] fatal error during setup: ${payload.message}${payload.stack ? `\n${payload.stack}` : ''}\n`
  try {
    appendFileSync(DEBUG_LOG_PATH, line)
  } catch {
    // Ignore write errors
  }
}

const crashLogger = {
  enabled: debugEnabled,
  logPath: DEBUG_LOG_PATH,
  log: logDebug,
}

export async function runCli(argv?: string[]): Promise<void> {
  const proc = runtimeGlobals().process
  if (!proc) throw new Error('runCli is only supported in Node')
  const env = proc.env as MutableEnvRecord

  // CLI UX defaults:
  // - Avoid multi-second event-loop stalls from precise token counting on long transcripts.
  //   (Can be overridden by users who explicitly want precision.)
  ensurePrefixedEnvValue(env, 'TOKEN_COUNT_MODE', 'estimate')
  // Fast-start defaults: skip non-critical startup IO (can be disabled via DIMCODE_FAST_START=0).
  ensurePrefixedEnvValue(env, 'FAST_START', '1')

  // 1-shot profiling mode: enable all key profilers with sane defaults.
  if (isOneShotEnabled()) {
    const oneShotDefaults: Array<[string, string]> = [
      ['PERF_LOG', '1'],
      ['PROFILE_LAYOUT_BREAKDOWN', '1'],
      ['PROFILE_RENDERERS', '1'],
      ['PROFILE_STREAM_MUTATIONS', '1'],
      ['PROFILE_TOOL_EXECUTION', '1'],
      ['EVENT_LOOP_LAG_LOG', '1'],
      ['EVENT_LOOP_LAG_LOG_PATH', '/tmp/dimcode-event-loop-lag.log'],
      ['EVENT_LOOP_LAG_INTERVAL_MS', '50'],
      ['EVENT_LOOP_LAG_WARN_MS', '120'],
      ['GC_LOG', '1'],
      ['GC_LOG_SLOW_MS', '20'],
      // TUI profiler lives in packages/tui and can capture multi-second stalls in render/write.
      ['PROFILE_TUI', '1'],
      ['PROFILE_TUI_LOG_DEST', 'file'],
      ['PROFILE_TUI_LOG_PATH', '/tmp/dimcode-profile-tui.log'],
      ['PROFILE_TUI_LOG_EVERY_MS', '200'],
      // Verbose trace log for “print every step” debugging.
      ['TRACE', '1'],
      ['TRACE_LOG_PATH', '/tmp/dimcode-trace.log'],
      ['TRACE_MAX_LINES_PER_SEC', '10000'],
      ['TRACE_LOG_MODE', 'async'],
    ]

    for (const [key, fallback] of oneShotDefaults)
      ensurePrefixedEnvValue(env, key, fallback)
  }

  if (isPerfEnabled())
    perfLog(`[BOOT] perf=1 oneshot=${isOneShotEnabled() ? 1 : 0}`)
  if (readPrefixedEnvFlag(env, 'TRACE') === '1') {
    traceLog({
      tag: 'BOOT',
      message: 'trace=1',
      payload: {
        pid: process.pid,
        argv: (argv ?? proc.argv?.slice(2) ?? []).slice(0, 16),
      },
    })
  }

  if (readPrefixedEnvFlag(env, 'EVENT_LOOP_LAG_LOG') === '1')
    startEventLoopLagLogger()

  startGcLogger()

  startMarkdownCacheCleanup()

  const argvList = argv ?? proc.argv?.slice(2) ?? []

  if (isHelpRequested(argvList)) {
    console.log(getHelpText())
    return
  }

  const args = parseCliArgs(argvList, { stdout: proc.stdout })

  const baseTitle =
    readPrefixedEnv(env, 'TERM_TITLE_BASE') ||
    args.app.trim() ||
    'dim'
  ensurePrefixedEnvValue(env, 'TERM_TITLE_BASE', baseTitle)
  saveTerminalTitle({
    stdout: proc.stdout,
    env,
  })
  setTerminalTitle(baseTitle, {
    stdout: proc.stdout,
    env,
  })

  if (args.debug) {
    setPrefixedEnvValue(env, 'DEBUG', '1')
    if (!debugEnabled) {
      debugEnabled = true
      crashLogger.enabled = true
      setupCrashHandlers(crashLogger)
    }
  }

  logDebug('[CLI] runCli() called')
  logDebug('[CLI] args', { app: args.app, cols: args.cols, rows: args.rows })

  if (args.approvals) {
    setPrefixedEnvValue(env, 'TOOL_APPROVALS', args.approvals)
  }

  const install: TerminalInstaller | undefined =
    args.app === 'dim'
      ? (app: TerminalAppInstance) => {
          const router = createDimSdkRouter({
            initialRoute: args.initialRoute,
          })
          app.use(router)
        }
      : undefined

  logDebug('[CLI] Before createDimSdkShellProps')
  const dimProps: DimSdkShellProps | null =
    args.app === 'dim'
      ? await createDimSdkShellProps({
          cwd: proc.cwd(),
          env,
        })
      : null
  const componentProps = dimProps ?? { cols: args.cols, rows: args.rows }
  logDebug('[CLI] After createDimSdkShellProps')
  logDebug('[CLI] About to call runTerminalUi')

  const cap = detectTerminalColorCapability({
    env,
    isTTY: Boolean(proc.stdout?.isTTY),
    platform: proc.platform,
  })
  const initialThemeMode = resolveInitialThemeMode(
    dimProps?.context.env ?? env,
  )
  const uiThemeSwitcher = createUiThemeSwitcher({
    mode: initialThemeMode,
    capability: cap,
  })
  const uiTheme = uiThemeSwitcher.context
  const defaultBg = uiTheme.token('bg.window')?.bg ?? 'black'
  const defaultStyle = uiTheme.style(['text.primary', 'bg.window'])
  const palette = uiTheme.palette ?? undefined
  let pendingThemeUpdate: ThemeUpdate | null = null
  let applyThemeUpdate: ((next: ThemeUpdate) => void) | null = null

  const onStoreCreated: DimSdkShellProps['onStoreCreated'] | undefined =
    args.app === 'dim'
      ? (store) => {
          const original = dimProps?.onStoreCreated
          if (typeof original === 'function') original(store)
          watch(
            () => store.state.ui.theme.mode,
            (mode) => {
              uiThemeSwitcher.setMode(mode)
              const nextDefaultBg = uiTheme.token('bg.window')?.bg ?? 'black'
              const nextDefaultStyle = uiTheme.style([
                'text.primary',
                'bg.window',
              ])
              const nextPalette = uiTheme.palette ?? undefined
              const update = {
                defaultStyle: nextDefaultStyle,
                defaultBg: nextDefaultBg,
                palette: nextPalette,
              }
              if (applyThemeUpdate) applyThemeUpdate(update)
              else pendingThemeUpdate = update
            },
            { immediate: true },
          )
        }
      : undefined

  const propsWithTheme =
    args.app === 'dim'
      ? { ...dimProps, onStoreCreated }
      : componentProps

  const installWithTheme: TerminalInstaller = (app) => {
    install?.(app)
    app.provide(DimSdkUiThemeKey, uiTheme)
  }

  try {
    await runTerminalUi({
      proc,
      cols: args.cols,
      rows: args.rows,
      sizeLocked: args.sizeLocked,
      component: DimSdkShell,
      props: propsWithTheme,
      install: installWithTheme,
      defaultStyle: args.app === 'dim' ? defaultStyle : undefined,
      palette: args.app === 'dim' ? palette : undefined,
      defaultBg: args.app === 'dim' ? defaultBg : undefined,
      onThemeReady: (update) => {
        applyThemeUpdate = update
        if (pendingThemeUpdate) {
          update(pendingThemeUpdate)
          pendingThemeUpdate = null
        }
      },
      altScreen: args.altScreen,
      enableMouse: args.enableMouse,
      recordPath: args.recordPath,
      replayPath: args.replayPath,
      snapshotPath: args.snapshotPath,
    })
  } finally {
    restoreTerminalTitle({
      stdout: proc.stdout,
      env,
    })
  }
}

export async function main(): Promise<void> {
  setupCrashHandlers(crashLogger)

  // If the user requested help, print and exit immediately.
  // This avoids leaving background tasks (like upgrade checks) running, which can keep
  // Bun-compiled binaries alive unexpectedly.
  if (isHelpRequested(process.argv.slice(2))) {
    console.log(getHelpText())
    try {
      process.stdin?.pause?.()
    } catch {
      // ignore
    }
    process.exit(0)
  }

  const argv = process.argv.slice(2)
  // Treat version flags as a top-level command only, so subcommands can reuse `-v`
  // (e.g. `dim server --verbose`).
  const resolved = resolveTopLevelSubcommand(argv)
  if (resolved.kind === 'version') {
    await printCliVersionInfo(process.env)
    try {
      process.stdin?.pause?.()
    } catch {
      // ignore
    }
    process.exit(0)
    return
  }
  if (resolved.kind === 'upgrade') {
    const npmPackage = getNpmPackageOverride()
    if (npmPackage) {
      await runNpmUpgradeCommand(resolved.argv, npmPackage)
    } else if (isSelfContainedBinary()) {
      await runBinaryUpgradeCommand(resolved.argv)
    } else {
      await runNpmUpgradeCommand(resolved.argv)
    }
    return
  }
  if (resolved.kind === 'acp') {
    const env = process.env
    // Keep the ACP stdio server out of the normal CLI startup path so bundled TUI
    // entrypoints do not accidentally attach an NDJSON stdin reader.
    const { runStdioAcpServer } = await import('@best-agent/server')
    await runStdioAcpServer({
      cwd: process.cwd(),
      env: {
        ACP_DEBUG: env.ACP_DEBUG ?? 'true',
      },
    })
    return
  }
  if (resolved.kind === 'auth') {
    if (!isNextApiAuthEnabled()) {
      console.error('The `dim auth` command is not available in this release.')
      process.exit(1)
      return
    }
    const exitCode = await runAuthCommand(resolved.argv)
    try {
      process.stdin?.pause?.()
    } catch {
      // ignore
    }
    process.exit(Number.isFinite(exitCode) ? exitCode : 1)
    return
  }
  if (resolved.kind === 'legacy_exec') {
    console.error(
      'The `dim exec` command was removed. Use `dim [--cwd dir] [--session id] "prompt"` instead.',
    )
    try {
      process.stdin?.pause?.()
    } catch {
      // ignore
    }
    process.exit(1)
    return
  }
  if (
    resolved.kind === 'prompt' ||
    (resolved.kind === 'none' && argv.length === 0 && !process.stdin?.isTTY)
  ) {
    const exitCode = await runExec(resolved.argv, process)
    try {
      process.stdin?.pause?.()
    } catch {
      // ignore
    }
    if (Number.isFinite(exitCode)) process.exit(exitCode)
    process.exit(1)
    return
  }

  if (await maybeRelaunchToUpdatedBinary()) return

  const reportUpgradeMessage = createUpgradeMessageReporter(queueUpgradeToast)

  const upgradePromise = checkUpgrade()
  upgradePromise.then(reportUpgradeMessage).catch((error) => {
    logDebug('[CLI] upgrade check failed', {
      error: error instanceof Error ? error.message : String(error),
    })
  })

  const upgradeWaitRaw = String(
    process.env[STARTUP_UPGRADE_WAIT_ENV] ?? '',
  ).trim()
  const upgradeWaitMs = upgradeWaitRaw ? Number(upgradeWaitRaw) : 0
  if (Number.isFinite(upgradeWaitMs) && upgradeWaitMs > 0) {
    const upgradeMessage = await withTimeout(
      upgradePromise,
      Math.min(upgradeWaitMs, NPM_UPGRADE_CHECK_TIMEOUT_MS),
    )
    if (upgradeMessage === null) {
      logDebug('[CLI] npm upgrade check timed out', {
        timeoutMs: upgradeWaitMs,
      })
    }
    reportUpgradeMessage(upgradeMessage)
  }

  const stopBackgroundUpgradeChecks = startBackgroundUpgradeChecks({
    intervalMs: resolveBackgroundUpgradeCheckIntervalMs(
      process.env,
    ),
    checkUpgrade,
    onMessage: reportUpgradeMessage,
    onError: (error) => {
      logDebug('[CLI] periodic upgrade check failed', {
        error: error instanceof Error ? error.message : String(error),
      })
    },
  })

  try {
    await runCli()
  } finally {
    stopBackgroundUpgradeChecks()
  }
}

if (import.meta.main || import.meta.url === `file://${process.argv[1]}`) {
  main().catch((err) => {
    logFatalSetupError(err)
    console.error('Fatal error during setup:', err)
    console.error(`Debug log written to: ${DEBUG_LOG_PATH}`)
    requestFatalExit(1)
  })
}
