'use client'

import * as React from 'react'
import type { Terminal as XTerm } from 'xterm'
import type { FitAddon } from '@xterm/addon-fit'
import { AlertCircle, RefreshCw, TerminalSquare, Wifi, WifiOff } from 'lucide-react'
import { cn } from '../../lib/utils'
import { TERMINAL_THEMES, type TerminalStatus, type TerminalTheme } from './themes'

/**
 * Imperative handle the PARENT uses to drive the terminal. The parent owns the
 * transport (a websocket, an SSE stream, …) and pumps inbound bytes in via
 * `write()` — the terminal never re-renders per byte.
 */
export interface TerminalHandle {
  /** Write raw output (including ANSI escapes) to the terminal. */
  write(data: string): void
  /** Clear the screen + scrollback. */
  clear(): void
  /** Re-fit the terminal to its container. */
  fit(): void
  /** Move keyboard focus into the terminal. */
  focus(): void
}

export interface TerminalProps {
  /** xterm colour theme. Defaults to `TERMINAL_THEMES.dark`. */
  theme?: TerminalTheme
  fontFamily?: string
  fontSize?: number
  scrollback?: number
  /** Connection status shown in the chrome's indicator (purely cosmetic). */
  status?: TerminalStatus
  /** Fired with keystrokes/data typed into the terminal. Wire to your transport. */
  onData?: (data: string) => void
  /** Fired when the terminal is resized (after a fit). Report to the PTY. */
  onResize?: (cols: number, rows: number) => void
  /** Fired once the terminal is initialised, handing back the imperative handle. */
  onReady?: (handle: TerminalHandle) => void
  /** Fired when the reconnect button is clicked (only shown when provided). */
  onReconnect?: () => void
  /** Show the header chrome (title, status indicator, reconnect). Default true. */
  showChrome?: boolean
  className?: string
  /** Optional text written to the terminal once, right after init. */
  banner?: string
}

type Loggable = unknown

function StatusIndicator({ status }: { status: TerminalStatus }) {
  switch (status) {
    case 'connected':
      return (
        <span className="flex items-center gap-1.5 text-emerald-500" role="status">
          <Wifi className="h-4 w-4" aria-hidden="true" />
          <span className="text-xs">Connected</span>
        </span>
      )
    case 'connecting':
      return (
        <span className="flex items-center gap-1.5 text-muted-foreground" role="status">
          <RefreshCw className="h-4 w-4 animate-spin" aria-hidden="true" />
          <span className="text-xs">Connecting…</span>
        </span>
      )
    case 'error':
      return (
        <span className="flex items-center gap-1.5 text-red-500" role="status">
          <AlertCircle className="h-4 w-4" aria-hidden="true" />
          <span className="text-xs">Error</span>
        </span>
      )
    case 'disconnected':
    default:
      return (
        <span className="flex items-center gap-1.5 text-muted-foreground" role="status">
          <WifiOff className="h-4 w-4" aria-hidden="true" />
          <span className="text-xs">Disconnected</span>
        </span>
      )
  }
}

/**
 * A presentational xterm.js terminal. Generic — it knows nothing about the
 * transport, the session model, or any app. The parent feeds output through the
 * `TerminalHandle` (via `onReady`) and receives keystrokes/resizes through
 * `onData`/`onResize`.
 *
 * The defensive init sequence (DOM-ready wait, render-service wait, safe fit, and
 * a React-StrictMode double-init guard) is ported verbatim from dev-hq's
 * TerminalInterface — it exists for real timing bugs, not decoration. xterm is
 * loaded via dynamic import so this module is SSR- and test-safe (importing it
 * never touches `xterm` at module-eval time).
 */
export function Terminal({
  theme = TERMINAL_THEMES.dark,
  fontFamily = 'Menlo, Monaco, "Courier New", monospace',
  fontSize = 14,
  scrollback = 10000,
  status = 'disconnected',
  onData,
  onResize,
  onReady,
  onReconnect,
  showChrome = true,
  className,
  banner,
}: TerminalProps) {
  const containerRef = React.useRef<HTMLDivElement>(null)
  const xtermRef = React.useRef<XTerm | null>(null)
  const fitAddonRef = React.useRef<FitAddon | null>(null)
  const isInitializedRef = React.useRef(false) // guard against StrictMode double-init
  const isMountedRef = React.useRef(true)
  const resizeTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)

  // Keep callbacks in refs so the (sessionUuid-less) mount effect stays stable and
  // xterm is never re-created just because a parent re-renders with a new closure.
  const onDataRef = React.useRef(onData)
  const onResizeRef = React.useRef(onResize)
  const onReadyRef = React.useRef(onReady)
  onDataRef.current = onData
  onResizeRef.current = onResize
  onReadyRef.current = onReady

  const [errorMessage, setErrorMessage] = React.useState<string | null>(null)

  // ---- defensive init helpers (ported from dev-hq, logging stripped) ----------

  const waitForDOMReady = React.useCallback((): Promise<void> => {
    return new Promise((resolve, reject) => {
      const container = containerRef.current
      if (!container) {
        reject(new Error('Terminal container ref not available'))
        return
      }
      if (container.offsetWidth > 0 && container.offsetHeight > 0) {
        resolve()
        return
      }
      const ro = new ResizeObserver((entries) => {
        for (const entry of entries) {
          if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
            ro.disconnect()
            resolve()
            return
          }
        }
      })
      ro.observe(container)
      setTimeout(() => {
        ro.disconnect()
        reject(new Error('Container dimensions timeout'))
      }, 5000)
    })
  }, [])

  // Poll until xterm's render service exists and the terminal is attached. Never
  // throws on timeout — the terminal is usable and fit() retries on resize.
  const waitForRenderService = React.useCallback(async (terminal: XTerm): Promise<void> => {
    const maxAttempts = 10
    const delay = 50
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      if (!isMountedRef.current) throw new Error('Component unmounted during initialization')
      try {
        const term = terminal as unknown as { _core?: { _renderService?: Loggable; element?: Loggable } }
        if (term._core?._renderService && term._core?.element) return
      } catch {
        /* render service not ready — keep polling */
      }
      await new Promise((r) => setTimeout(r, delay))
    }
  }, [])

  const safeFit = React.useCallback((fitAddon: FitAddon): void => {
    if (!isMountedRef.current || !containerRef.current) return
    try {
      fitAddon.fit()
    } catch {
      /* fit failures are timing-related; retries happen on resize */
    }
  }, [])

  const handleResize = React.useCallback(() => {
    if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current)
    resizeTimeoutRef.current = setTimeout(() => {
      if (fitAddonRef.current && xtermRef.current && containerRef.current && isMountedRef.current) {
        try {
          fitAddonRef.current.fit()
        } catch {
          /* ignore — retries on next resize */
        }
      }
    }, 150)
  }, [])

  // Mount / unmount: create + dispose xterm exactly once per mount.
  React.useEffect(() => {
    isMountedRef.current = true
    let disposed = false
    let cleanupCalled = false

    const init = async () => {
      if (!containerRef.current || xtermRef.current || isInitializedRef.current) return
      isInitializedRef.current = true
      try {
        // Dynamic import keeps xterm out of the SSR/test module graph.
        const [{ Terminal: XTermCtor }, { FitAddon: FitAddonCtor }] = await Promise.all([
          import('xterm'),
          import('@xterm/addon-fit'),
        ])
        if (!isMountedRef.current || disposed) return

        await waitForDOMReady()
        if (!isMountedRef.current || disposed) return

        const terminal = new XTermCtor({
          cursorBlink: true,
          fontSize,
          fontFamily,
          fontWeight: '400',
          fontWeightBold: '700',
          lineHeight: 1.2,
          letterSpacing: 0,
          theme,
          scrollback,
          allowProposedApi: true,
          allowTransparency: true,
          windowsMode: false,
        })
        const fitAddon = new FitAddonCtor()
        terminal.loadAddon(fitAddon)
        terminal.open(containerRef.current)

        xtermRef.current = terminal
        fitAddonRef.current = fitAddon

        try {
          await waitForRenderService(terminal)
        } catch {
          /* continue — terminal still usable */
        }
        if (!isMountedRef.current || disposed) {
          terminal.dispose()
          return
        }

        safeFit(fitAddon)

        terminal.onData((data: string) => onDataRef.current?.(data))
        terminal.onResize(({ cols, rows }: { cols: number; rows: number }) =>
          onResizeRef.current?.(cols, rows),
        )

        if (banner) terminal.writeln(banner)

        const handle: TerminalHandle = {
          write: (d) => terminal.write(d),
          clear: () => terminal.clear(),
          fit: () => safeFit(fitAddon),
          focus: () => terminal.focus(),
        }
        onReadyRef.current?.(handle)
      } catch (error) {
        const err = error instanceof Error ? error : new Error('Failed to initialize terminal')
        setErrorMessage(err.message)
        isInitializedRef.current = false // allow retry
      }
    }

    // Small delay so refs are set and the container is laid out.
    const initTimer = setTimeout(init, 100)

    return () => {
      clearTimeout(initTimer)
      if (cleanupCalled) return // StrictMode runs cleanup twice in dev
      cleanupCalled = true
      disposed = true
      isMountedRef.current = false
      if (resizeTimeoutRef.current) {
        clearTimeout(resizeTimeoutRef.current)
        resizeTimeoutRef.current = null
      }
      if (xtermRef.current) {
        xtermRef.current.dispose()
        xtermRef.current = null
      }
      fitAddonRef.current = null
      isInitializedRef.current = false
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  // Live-apply theme changes without re-creating the terminal.
  React.useEffect(() => {
    if (xtermRef.current) xtermRef.current.options.theme = theme
  }, [theme])

  React.useEffect(() => {
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [handleResize])

  const showReconnect = !!onReconnect && (status === 'disconnected' || status === 'error')

  return (
    <div className={cn('flex h-full flex-col overflow-hidden rounded-lg border border-border bg-card', className)}>
      {showChrome && (
        <div className="flex flex-shrink-0 items-center justify-between border-b border-border bg-muted/40 px-4 py-2">
          <div className="flex items-center gap-3">
            <TerminalSquare className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
            <span className="text-sm font-medium text-foreground">Terminal</span>
            <StatusIndicator status={status} />
          </div>
          {showReconnect && (
            <button
              type="button"
              onClick={onReconnect}
              aria-label="Reconnect"
              title="Reconnect"
              className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
            >
              <RefreshCw className="h-4 w-4" aria-hidden="true" />
            </button>
          )}
        </div>
      )}

      {errorMessage && (
        <div className="flex flex-shrink-0 items-center gap-2 border-b border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
          <AlertCircle className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
          <span>{errorMessage}</span>
        </div>
      )}

      <div
        ref={containerRef}
        data-testid="terminal-container"
        className="min-h-[300px] w-full flex-1 overflow-hidden"
        style={{ backgroundColor: theme.background }}
      />
    </div>
  )
}

/** Skeleton placeholder shown while a terminal surface is loading. */
export function TerminalSkeleton({ className }: { className?: string }) {
  return (
    <div className={cn('overflow-hidden rounded-lg border border-border bg-card', className)}>
      <div className="flex items-center justify-between border-b border-border bg-muted/40 px-4 py-2">
        <div className="flex items-center gap-3">
          <div className="h-4 w-4 animate-pulse rounded bg-muted" />
          <div className="h-4 w-20 animate-pulse rounded bg-muted" />
          <div className="h-4 w-24 animate-pulse rounded bg-muted" />
        </div>
      </div>
      <div className="space-y-2 p-4" style={{ minHeight: 300 }}>
        {Array.from({ length: 10 }).map((_, i) => (
          <div
            key={i}
            className="h-4 animate-pulse rounded bg-muted"
            style={{ width: `${((i * 37) % 40) + 55}%` }}
          />
        ))}
      </div>
    </div>
  )
}
