import type {
  AnsiColorName,
  CliEventManager,
  Style,
  StdoutRenderer,
  Terminal,
  TerminalApp,
  TerminalEventRecord,
} from '@simon_he/vue-tui'
import type { Component } from 'vue'

import { debugLog as writeDebugLog } from '#cli/core/debug-log'
import {
  createNodePathPickerProvider,
  createStdinDriver,
  createStdoutRenderer,
  createTerminalApp,
  readEventLog,
  writeEventLog,
  writeSnapshot,
} from '@simon_he/vue-tui'
import { nextTick } from 'vue'
import {
  teardownActiveSession,
  updateActiveSessionVisibility,
} from './active-session'
import { requestExit, setExitNow } from './exit-controller'
import { nowMs, sleep } from './time'

const DISCORD_INVITE_URL = 'https://discord.com/invite/uaSkW9nzQ6'
const OSC8_CLOSE = '\u001B]8;;\u0007'
type CliProcess = NodeJS.Process
type ThemePalette = Partial<Record<AnsiColorName, string>>
type RuntimeGlobals = typeof globalThis & {
  Bun?: unknown
}

function formatTerminalHyperlink(label: string, href: string): string {
  return `\u001B]8;;${href}\u0007${label}${OSC8_CLOSE}`
}

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

function unrefTimer(timer: ReturnType<typeof setInterval> | ReturnType<typeof setTimeout> | null): void {
  timer?.unref?.()
}

function buildExitFeedbackMessage(proc: CliProcess): string {
  const discordLink = proc.stdout.isTTY
    ? `${formatTerminalHyperlink('Discord', DISCORD_INVITE_URL)} (${DISCORD_INVITE_URL})`
    : DISCORD_INVITE_URL
  return [
    '',
    'Thanks for using dim. 👋',
    'Need help or want to share feedback? 💬',
    `Join our Discord: ${discordLink} 🚀`,
    '',
  ].join('\n')
}

function writeExitFeedback(proc: CliProcess): void {
  try {
    proc.stdout.write(buildExitFeedbackMessage(proc))
  }
  catch {
    // ignore
  }
}

export type CliApp = Readonly<{
  app: TerminalApp['app']
  terminal: Terminal
  events: CliEventManager
  scheduler: { invalidate: () => void, flush: () => void }
  getImeAnchor: TerminalApp['getImeAnchor']
  defaultStyle: TerminalApp['defaultStyle']
  dispose: () => void
}>

export function createCliApp(
  options: Readonly<{
    cols: number
    rows: number
    defaultStyle?: Style
    component: Component
    props?: Record<string, unknown>
    install?: (app: TerminalApp['app']) => void
  }>,
): CliApp {
  const app = createTerminalApp({
    cols: options.cols,
    rows: options.rows,
    component: options.component,
    props: options.props,
    defaultStyle: options.defaultStyle,
    pathPickerProvider: createNodePathPickerProvider(),
  })

  // Prevent Vue warnings/errors from corrupting the full-screen terminal UI by writing to stdout/stderr.
  // When DIMCODE_DEBUG is enabled, route them to the debug log instead.
  try {
    app.app.config.warnHandler = (
      msg: unknown,
      instance: unknown,
      trace: unknown,
    ) => {
      void instance
      writeDebugLog('vue_warn', {
        msg: String(msg ?? ''),
        trace: trace ? String(trace) : '',
      })
    }
    app.app.config.errorHandler = (
      err: unknown,
      instance: unknown,
      info: unknown,
    ) => {
      void instance
      writeDebugLog('vue_error', {
        info: info ? String(info) : '',
        error:
          err instanceof Error
            ? { message: err.message, stack: err.stack }
            : String(err),
      })
    }
  }
  catch {
    // ignore
  }

  options.install?.(app.app)
  app.mount()

  return {
    app: app.app,
    terminal: app.terminal,
    events: app.events,
    scheduler: app.scheduler,
    getImeAnchor: app.getImeAnchor,
    defaultStyle: app.defaultStyle,
    dispose: app.dispose,
  }
}

export type ThemeUpdate = Readonly<{
  defaultStyle?: Style
  palette?: ThemePalette
  defaultBg?: AnsiColorName
}>

async function replay(
  events: CliEventManager,
  scheduler: CliApp['scheduler'],
  list: TerminalEventRecord[],
): Promise<void> {
  let prevTime: number | null = null
  for (const e of list) {
    const t = typeof e.time === 'number' ? e.time : null
    if (t != null && prevTime != null) {
      const delta = Math.max(0, t - prevTime)
      // Respect recorded timings so app-side timers (mock "thinking", approvals, etc.) line up with clicks.
      await sleep(Math.min(delta, 5_000))
    }
    if (t != null)
      prevTime = t
    events.dispatch(e)
    await nextTick()
    scheduler.flush()
  }
}

export async function runTerminalUi(
  opts: Readonly<{
    proc: CliProcess
    cols: number
    rows: number
    sizeLocked: boolean
    component: Component
    props?: Record<string, unknown>
    install?: (app: TerminalApp['app']) => void
    defaultStyle?: Style
    palette?: ThemePalette
    defaultBg?: AnsiColorName
    onThemeReady?: (update: (next: ThemeUpdate) => void) => void
    altScreen: boolean
    enableMouse: boolean
    recordPath: string | null
    replayPath: string | null
    snapshotPath: string | null
  }>,
): Promise<void> {
  const app = createCliApp({
    cols: opts.cols,
    rows: opts.rows,
    component: opts.component,
    props: opts.props,
    install: opts.install,
    defaultStyle: opts.defaultStyle,
  })
  const createOut = () =>
    createStdoutRenderer(app.terminal, {
      output: opts.proc.stdout,
      altScreen: opts.altScreen,
      palette: opts.palette,
      trackResize: !opts.sizeLocked,
      // Keep the native terminal cursor hidden; the UI draws its own caret in TInput.
      // (Showing the native cursor causes visible flicker / double-caret artifacts.)
      hideCursor: true,
      defaultBg: opts.defaultBg,
      // Pass getImeAnchor so cursor position is included in render() output.
      // This eliminates separate setCursor() calls and prevents flickering in Bun binaries
      // by ensuring everything is written in a single atomic out.write() call.
      getImeAnchor: app.getImeAnchor,
    })

  let out: StdoutRenderer | null = null
  let lastNativeCursor: { x: number, y: number } | null = null
  const updateTheme = (next: ThemeUpdate) => {
    if (next.defaultStyle)
      app.defaultStyle.value = next.defaultStyle
    if (out?.updateTheme)
      out.updateTheme({ defaultBg: next.defaultBg, palette: next.palette })
    app.scheduler.invalidate()
  }
  opts.onThemeReady?.(updateTheme)

  const log: TerminalEventRecord[] = []
  const t0 = nowMs()

  const dispatch = (e: TerminalEventRecord) => {
    const ev: TerminalEventRecord
      = e.time == null ? { ...e, time: nowMs() - t0 } : e
    log.push(ev)
    const result = app.events.dispatch(ev)
    const anchor = app.getImeAnchor()
    if (out && anchor) {
      const x = Math.floor(anchor.cellX)
      const y = Math.floor(anchor.cellY)
      if (
        !lastNativeCursor
        || lastNativeCursor.x !== x
        || lastNativeCursor.y !== y
      ) {
        out.setCursor(x, y)
        lastNativeCursor = { x, y }
      }
    }
    else {
      lastNativeCursor = null
    }
    return result
  }

  let driver: ReturnType<typeof createStdinDriver> | null = null

  const takeOverStdin = () => {
    const stdin = opts.proc.stdin

    // In Bun-compiled binaries we sometimes end up with stray stdin consumers (e.g. JSON-line
    // parsers from other transports) which will treat pasted text as JSON and spam errors.
    // The TUI must be the sole consumer of stdin while running in raw mode.
    const isBun = runtimeGlobals().Bun != null
    if (!isBun)
      return

    try {
      const dataListeners = stdin.listeners('data')
      const readableListeners = stdin.listeners('readable')
      if (dataListeners.length > 0 || readableListeners.length > 0) {
        stdin.removeAllListeners('data')
        stdin.removeAllListeners('readable')
      }
    }
    catch {
      // ignore
    }
  }

  const procEnv = opts.proc.env
  const isGhostty = 'GHOSTTY_RESOURCES_DIR' in procEnv
  let keepAliveInterval: ReturnType<typeof setInterval> | null = null
  let hardExitTimer: ReturnType<typeof setTimeout> | null = null
  let softExitTimer: ReturnType<typeof setTimeout> | null = null
  let schedulerInvalidateWrapped = false
  let hasPendingWork = false

  const stopKeepAlive = () => {
    if (keepAliveInterval) {
      clearInterval(keepAliveInterval)
      keepAliveInterval = null
    }
  }

  const ensureSchedulerInvalidateWrapped = () => {
    if (schedulerInvalidateWrapped)
      return
    schedulerInvalidateWrapped = true
    const originalInvalidate = app.scheduler.invalidate
    app.scheduler.invalidate = () => {
      hasPendingWork = true
      originalInvalidate()
    }
  }

  const startKeepAlive = () => {
    if (!isGhostty || keepAliveInterval)
      return
    ensureSchedulerInvalidateWrapped()
    keepAliveInterval = setInterval(() => {
      if (hasPendingWork) {
        app.scheduler.flush()
        hasPendingWork = false
      }
    }, 100)
  }

  const scheduleHardExit = () => {
    if (hardExitTimer)
      return
    const code = typeof opts.proc.exitCode === 'number' ? opts.proc.exitCode : 0
    hardExitTimer = setTimeout(() => {
      try {
        opts.proc.exit(code)
      }
      catch {
        // ignore
      }
    }, 1500)
    unrefTimer(hardExitTimer)
  }

  const scheduleSoftExit = (delayMs = 50) => {
    if (softExitTimer)
      return
    const code = typeof opts.proc.exitCode === 'number' ? opts.proc.exitCode : 0
    softExitTimer = setTimeout(() => {
      try {
        opts.proc.exit(code)
      }
      catch {
        // ignore
      }
    }, delayMs)
    unrefTimer(softExitTimer)
  }

  const disposeTerminalIo = () => {
    stopKeepAlive()
    if (driver) {
      driver.dispose()
      driver = null
    }
    if (out) {
      out.dispose()
      out = null
    }
  }

  let appDisposed = false
  let exitFeedbackShown = false
  let activeSessionTornDown = false
  const cleanupActiveSession = () => {
    if (activeSessionTornDown)
      return
    activeSessionTornDown = true
    teardownActiveSession({
      source: 'process_exit',
      env: procEnv,
    })
  }
  const disposeAll = () => {
    disposeTerminalIo()
    if (!appDisposed) {
      app.dispose()
      appDisposed = true
    }
  }

  const exit = () => {
    void (async () => {
      if (opts.proc.exitCode == null)
        opts.proc.exitCode = 0
      const snapshotLines = opts.snapshotPath
        ? app.terminal.snapshot().lines
        : null
      cleanupActiveSession()
      disposeAll()
      if (!exitFeedbackShown) {
        exitFeedbackShown = true
        writeExitFeedback(opts.proc)
      }
      if (opts.recordPath)
        await writeEventLog(opts.recordPath, log)
      if (opts.snapshotPath && snapshotLines)
        await writeSnapshot(opts.snapshotPath, snapshotLines)
      scheduleSoftExit()
      scheduleHardExit()
    })()
  }
  setExitNow(exit)

  // Terminal state recovery: handle normal exit and crash scenarios
  opts.proc.on('exit', () => {
    cleanupActiveSession()
    disposeAll()
  })

  opts.proc.on('beforeExit', () => {
    cleanupActiveSession()
    disposeAll()
  })

  // Handle Ctrl+Z (suspend) and resume to restore terminal state.
  // Important: if we install a SIGTSTP handler, the default "stop" action is suppressed,
  // so we must temporarily remove the handler before re-sending SIGTSTP to ourselves.
  const onSigTstp = () => {
    updateActiveSessionVisibility({
      visibility: 'suspended',
      source: 'sigtstp',
      env: procEnv,
    })
    disposeTerminalIo()
    try {
      opts.proc.removeListener('SIGTSTP', onSigTstp)
    }
    catch {
      // ignore
    }
    try {
      opts.proc.kill(opts.proc.pid, 'SIGTSTP')
    }
    catch {
      // ignore
    }
  }

  const ensureSigTstpHandlerInstalled = () => {
    try {
      const listeners = opts.proc.listeners('SIGTSTP')
      if (Array.isArray(listeners) && listeners.includes(onSigTstp))
        return
    }
    catch {
      // ignore
    }
    opts.proc.on('SIGTSTP', onSigTstp)
  }

  ensureSigTstpHandlerInstalled()

  opts.proc.on('SIGCONT', () => {
    ensureSigTstpHandlerInstalled()
    // Restore terminal state after resume.
    if (!out)
      out = createOut()
    if (!driver && !opts.replayPath) {
      takeOverStdin()
      driver = createStdinDriver({
        stdin: opts.proc.stdin,
        stdout: opts.proc.stdout,
        dispatch,
        enableMouse: opts.enableMouse,
        enableMouseMotion: opts.enableMouse,
        onTerminalFocusChange: (focused) => {
          updateActiveSessionVisibility({
            visibility: focused ? 'focused' : 'blurred',
            source: focused ? 'terminal_focus_in' : 'terminal_focus_out',
            env: procEnv,
          })
        },
        onExit: () => requestExit(),
      })
    }
    if (!out)
      out = createOut()
    if (driver)
      startKeepAlive()

    updateActiveSessionVisibility({
      visibility: 'focused',
      source: 'sigcont',
      env: procEnv,
    })

    // Force a redraw to restore the screen
    app.scheduler.invalidate()
    app.scheduler.flush()
  })

  if (opts.replayPath) {
    out = createOut()
    opts.onThemeReady?.(updateTheme)
    // Paint the initial UI before replaying recorded events.
    app.scheduler.flush()
    // Ensure mount-time watchEffects (autoFocus, layout, etc.) have applied before replaying.
    await nextTick()
    await nextTick()
    app.scheduler.flush()

    const events = await readEventLog(opts.replayPath)
    await replay(app.events, app.scheduler, events)
    // Let any pending timers (e.g. mock "thinking" delays) settle before snapshotting.
    await sleep(800)
    await nextTick()
    app.scheduler.flush()
    if (opts.snapshotPath)
      await writeSnapshot(opts.snapshotPath, app.terminal.snapshot().lines)
    disposeAll()
    return
  }

  const createDriver = () =>
    createStdinDriver({
      stdin: opts.proc.stdin,
      stdout: opts.proc.stdout,
      dispatch,
      enableMouse: opts.enableMouse,
      enableMouseMotion: opts.enableMouse,
      onTerminalFocusChange: (focused) => {
        updateActiveSessionVisibility({
          visibility: focused ? 'focused' : 'blurred',
          source: focused ? 'terminal_focus_in' : 'terminal_focus_out',
          env: procEnv,
        })
      },
      onExit: () => requestExit(),
    })

  out = createOut()
  opts.onThemeReady?.(updateTheme)
  app.scheduler.flush()
  takeOverStdin()
  driver = createDriver()

  if (isGhostty)
    startKeepAlive()

  opts.proc.on('SIGINT', () => requestExit())
  opts.proc.on('SIGTERM', () => requestExit())

  if (isGhostty) {
    await new Promise(() => {
      // Keep the process alive while the TUI owns the terminal.
    })
  }
}
