import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from 'react'
import { Terminal as XTerm } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'
import { getToken } from './api'
import { useAuth } from './AuthContext'
import {
  TERM_THEMES,
  loadPrefs,
  savePrefs,
  type TabState,
  type TerminalPrefs,
} from './terminalPrefs'

/**
 * One live terminal pane: its xterm instance, fit addon, WebSocket, and the
 * persistent wrapper DOM node that xterm is attached to. The wrapper lives in
 * a hidden container at the app root and is re-parented into the Terminal page
 * grid when that page is mounted, so the xterm instance and its WebSocket
 * survive route changes (the page unmounting no longer disposes them).
 */
interface PaneSession {
  paneId: number
  wrapper: HTMLDivElement
  term: XTerm
  fit: FitAddon
  ws: WebSocket | null
  hostId: number | null
  connected: boolean
  /** Last tmux session list fetched for the current host. */
  tmuxSessions: string[]
  /** Currently-selected tmux option in the pane header (raw select value). */
  selectedTmux: string
  /** Disposes the term's onData/onResize listeners before re-wiring on reconnect. */
  disposeListeners: (() => void) | null
}

let nextId = 1
const genId = () => nextId++

function newTab(): TabState {
  return { id: genId(), name: 'Terminal', panes: [{ id: genId(), hostId: null }] }
}

interface TerminalSessionContextValue {
  tabs: TabState[]
  activeTabId: number
  activePaneId: number | null
  prefs: TerminalPrefs
  setActiveTabId: (id: number) => void
  setActivePaneId: (id: number | null) => void
  setPrefs: (update: (p: TerminalPrefs) => TerminalPrefs) => void
  addTab: () => void
  closeTab: (id: number) => void
  setPaneCount: (count: number) => void
  setPaneHost: (paneId: number, hostId: number) => void
  /** Mount a pane's persistent DOM node into a grid cell; returns a detach fn. */
  attachPane: (paneId: number, cell: HTMLElement) => () => void
  /** Read-only snapshot of a pane's live connection state (for header UI). */
  getPaneInfo: (paneId: number) => { connected: boolean; tmuxSessions: string[]; selectedTmux: string }
  /** Reconnect a pane to a tmux session (or plain shell) via the header select. */
  reconnectTmux: (paneId: number, rawSelectValue: string) => void
  /** Version counter that bumps whenever live pane state changes, to trigger re-render. */
  version: number
}

const TerminalSessionContext = createContext<TerminalSessionContextValue | null>(null)

export function TerminalSessionProvider({ children }: { children: ReactNode }) {
  const { status } = useAuth()

  const [tabs, setTabs] = useState<TabState[]>(() => [newTab()])
  const [activeTabId, setActiveTabId] = useState(() => tabs[0].id)
  const [activePaneId, setActivePaneId] = useState<number | null>(null)
  const [prefs, setPrefsState] = useState<TerminalPrefs>(loadPrefs)
  const [version, setVersion] = useState(0)

  // Hidden host for offscreen pane wrappers; created lazily on first use (never
  // during render — only from effects/handlers — to satisfy the rules of refs).
  const hiddenRootRef = useRef<HTMLDivElement | null>(null)
  function getHiddenRoot(): HTMLDivElement {
    let el = hiddenRootRef.current
    if (!el) {
      el = document.createElement('div')
      el.style.position = 'absolute'
      el.style.width = '0'
      el.style.height = '0'
      el.style.overflow = 'hidden'
      el.style.left = '-99999px'
      el.style.top = '0'
      hiddenRootRef.current = el
    }
    if (!el.isConnected) document.body.appendChild(el)
    return el
  }

  const sessionsRef = useRef<Map<number, PaneSession>>(new Map())

  useEffect(() => {
    const hidden = getHiddenRoot()
    return () => {
      if (hidden.isConnected) hidden.remove()
    }
  }, [])

  useEffect(() => savePrefs(prefs), [prefs])

  const bump = () => setVersion((v) => v + 1)

  // --- Session lifecycle ----------------------------------------------------

  function ensureSession(paneId: number): PaneSession {
    const existing = sessionsRef.current.get(paneId)
    if (existing) return existing

    const wrapper = document.createElement('div')
    wrapper.style.width = '100%'
    wrapper.style.height = '100%'
    wrapper.style.minHeight = '0'
    getHiddenRoot().appendChild(wrapper)

    const theme = TERM_THEMES.find((t) => t.name === prefs.themeName) ?? TERM_THEMES[0]
    const term = new XTerm({
      cursorBlink: true,
      fontSize: prefs.fontSize,
      fontFamily: prefs.fontFamily,
      theme: { background: theme.background, foreground: theme.foreground, cursor: theme.cursor },
    })
    const fit = new FitAddon()
    term.loadAddon(fit)
    term.open(wrapper)

    const session: PaneSession = {
      paneId,
      wrapper,
      term,
      fit,
      ws: null,
      hostId: null,
      connected: false,
      tmuxSessions: [],
      selectedTmux: '',
      disposeListeners: null,
    }
    sessionsRef.current.set(paneId, session)
    return session
  }

  function applyPrefsToSession(s: PaneSession) {
    const theme = TERM_THEMES.find((t) => t.name === prefs.themeName) ?? TERM_THEMES[0]
    s.term.options.fontSize = prefs.fontSize
    s.term.options.fontFamily = prefs.fontFamily
    s.term.options.theme = { background: theme.background, foreground: theme.foreground, cursor: theme.cursor }
    try {
      s.fit.fit()
    } catch {
      /* container may be detached */
    }
  }

  function fetchTmuxSessions(s: PaneSession, id: number) {
    s.tmuxSessions = []
    bump()
    const token = getToken()
    const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
    const ws = new WebSocket(`${proto}://${window.location.host}/api/terminal?token=${encodeURIComponent(token ?? '')}`)
    ws.onopen = () => ws.send(JSON.stringify({ type: 'list_tmux', integrationId: id }))
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data)
      if (msg.type === 'tmux_sessions') {
        s.tmuxSessions = msg.sessions ?? []
        bump()
        ws.close()
      }
    }
  }

  function connect(s: PaneSession, id: number, tmuxSession?: string) {
    s.disposeListeners?.()
    s.ws?.close()
    s.connected = false
    bump()

    const term = s.term
    term.reset()
    term.writeln('Connecting…')

    const token = getToken()
    const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
    const ws = new WebSocket(`${proto}://${window.location.host}/api/terminal?token=${encodeURIComponent(token ?? '')}`)
    s.ws = ws

    ws.onopen = () => {
      ws.send(JSON.stringify({ type: 'connect', integrationId: id, cols: term.cols, rows: term.rows, tmuxSession }))
    }
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data)
      if (msg.type === 'connected') {
        s.connected = true
        bump()
        term.reset()
      } else if (msg.type === 'data') {
        term.write(msg.data)
      } else if (msg.type === 'error') {
        term.writeln(`\r\n\x1b[31mError: ${msg.message}\x1b[0m`)
        s.connected = false
        bump()
      } else if (msg.type === 'closed') {
        term.writeln('\r\n\x1b[33mConnection closed.\x1b[0m')
        s.connected = false
        bump()
      }
    }
    ws.onclose = () => {
      s.connected = false
      bump()
    }

    const dataDisp = term.onData((data) => {
      if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'input', data }))
    })
    const resizeDisp = term.onResize(({ cols, rows }) => {
      if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'resize', cols, rows }))
    })
    s.disposeListeners = () => {
      dataDisp.dispose()
      resizeDisp.dispose()
    }
  }

  function destroySession(paneId: number) {
    const s = sessionsRef.current.get(paneId)
    if (!s) return
    s.disposeListeners?.()
    s.ws?.close()
    s.term.dispose()
    s.wrapper.remove()
    sessionsRef.current.delete(paneId)
  }

  function destroyAllSessions() {
    for (const paneId of [...sessionsRef.current.keys()]) destroySession(paneId)
  }

  // Tear everything down on logout (sessions must not outlive the auth session).
  useEffect(() => {
    if (status === 'logged-out' || status === 'needs-setup') {
      destroyAllSessions()
      // Defer the state reset so we don't call setState synchronously inside the
      // effect body (which would trigger a cascading render).
      queueMicrotask(() => {
        const fresh = newTab()
        setTabs([fresh])
        setActiveTabId(fresh.id)
        setActivePaneId(null)
      })
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [status])

  // --- Public actions -------------------------------------------------------

  function setPrefs(update: (p: TerminalPrefs) => TerminalPrefs) {
    setPrefsState((prev) => {
      const next = update(prev)
      // Re-style any live sessions in place rather than recreating them.
      queueMicrotask(() => {
        for (const s of sessionsRef.current.values()) applyPrefsToSession(s)
      })
      return next
    })
  }

  function addTab() {
    const tab = newTab()
    setTabs((prev) => [...prev, tab])
    setActiveTabId(tab.id)
  }

  function closeTab(id: number) {
    setTabs((prev) => {
      const closing = prev.find((t) => t.id === id)
      // Closing a tab disconnects its panes' sessions.
      closing?.panes.forEach((p) => destroySession(p.id))
      const next = prev.filter((t) => t.id !== id)
      if (next.length === 0) {
        const t = newTab()
        setActiveTabId(t.id)
        return [t]
      }
      if (id === activeTabId) setActiveTabId(next[0].id)
      return next
    })
  }

  function updateActiveTab(fn: (tab: TabState) => TabState) {
    setTabs((prev) => prev.map((t) => (t.id === activeTabId ? fn(t) : t)))
  }

  function setPaneCount(count: number) {
    updateActiveTab((tab) => {
      const panes = [...tab.panes]
      while (panes.length < count) panes.push({ id: genId(), hostId: null })
      while (panes.length > count) {
        const removed = panes.pop()
        // Reducing the grid disconnects the dropped pane's session.
        if (removed) destroySession(removed.id)
      }
      return { ...tab, panes }
    })
  }

  function setPaneHost(paneId: number, hostId: number) {
    updateActiveTab((tab) => ({
      ...tab,
      panes: tab.panes.map((p) => (p.id === paneId ? { ...p, hostId } : p)),
    }))
    setActivePaneId(paneId)

    const s = ensureSession(paneId)
    if (s.hostId === hostId) return
    s.hostId = hostId
    s.selectedTmux = ''
    fetchTmuxSessions(s, hostId)
    connect(s, hostId)
  }

  function attachPane(paneId: number, cell: HTMLElement): () => void {
    const s = ensureSession(paneId)
    cell.appendChild(s.wrapper)
    // Defer the fit until layout has settled in the new parent.
    requestAnimationFrame(() => {
      try {
        s.fit.fit()
      } catch {
        /* ignore */
      }
    })
    return () => {
      // On page unmount, move the wrapper back to the hidden root instead of
      // disposing it, so the xterm + WebSocket keep running in the background.
      const hidden = getHiddenRoot()
      if (s.wrapper.parentElement !== hidden) hidden.appendChild(s.wrapper)
    }
  }

  function getPaneInfo(paneId: number) {
    const s = sessionsRef.current.get(paneId)
    return {
      connected: s?.connected ?? false,
      tmuxSessions: s?.tmuxSessions ?? [],
      selectedTmux: s?.selectedTmux ?? '',
    }
  }

  function reconnectTmux(paneId: number, rawSelectValue: string) {
    const s = sessionsRef.current.get(paneId)
    if (!s || s.hostId === null) return
    s.selectedTmux = rawSelectValue
    const value = rawSelectValue === '__new__' ? `archnest-${Date.now().toString(36)}` : rawSelectValue
    connect(s, s.hostId, value || undefined)
  }

  const value: TerminalSessionContextValue = {
    tabs,
    activeTabId,
    activePaneId,
    prefs,
    setActiveTabId,
    setActivePaneId,
    setPrefs,
    addTab,
    closeTab,
    setPaneCount,
    setPaneHost,
    attachPane,
    getPaneInfo,
    reconnectTmux,
    version,
  }

  return <TerminalSessionContext.Provider value={value}>{children}</TerminalSessionContext.Provider>
}

export function useTerminalSessions() {
  const ctx = useContext(TerminalSessionContext)
  if (!ctx) throw new Error('useTerminalSessions must be used within TerminalSessionProvider')
  return ctx
}
