/**
 * T4 — mount xterm + FitAddon and drive it through an abstract `TerminalTransport`.
 *
 * Reproduces the base byte semantics against the SEAM (not `public/`): Enter → `\r` (0x0D, handled
 * by xterm's own onData), `resize` as its OWN protocol frame (triggering `TIOCSWINSZ`/SIGWINCH so
 * full-screen TUIs redraw), and `fit()` only after the container has real dimensions (calling it
 * while `display:none` yields NaN — base Gotcha).
 *
 * The terminal is injected (DI seam) so tests run in jsdom without a real canvas-backed xterm; the
 * default factory wires `@xterm/xterm` + `@xterm/addon-fit`.
 */
import { encodeClientMessage, decodeServerMessage } from './protocol'
import type { TerminalTransport } from './ws-transport'

/** Minimal xterm surface this view needs (keeps tests mock-able, avoids canvas in jsdom). */
export interface TerminalLike {
  readonly cols: number
  readonly rows: number
  open(container: HTMLElement): void
  write(data: string): void
  onData(cb: (data: string) => void): void
  fit(): void
  dispose(): void
}

export interface TerminalViewDeps {
  /** Factory for the terminal; default lazily loads xterm + FitAddon. */
  readonly createTerminal?: () => TerminalLike
}

/** True only when the element has real, non-zero layout dimensions (guards NaN fit — Gotcha). */
function hasRealDimensions(el: HTMLElement): boolean {
  return el.clientWidth > 0 && el.clientHeight > 0
}

async function defaultTerminal(): Promise<TerminalLike> {
  const [{ Terminal }, { FitAddon }] = await Promise.all([
    import('@xterm/xterm'),
    import('@xterm/addon-fit'),
  ])
  const term = new Terminal({ convertEol: false })
  const fit = new FitAddon()
  term.loadAddon(fit)
  return {
    get cols() {
      return term.cols
    },
    get rows() {
      return term.rows
    },
    open: (container) => term.open(container),
    write: (data) => term.write(data),
    onData: (cb) => {
      term.onData(cb)
    },
    fit: () => fit.fit(),
    dispose: () => term.dispose(),
  }
}

/**
 * Mount the terminal view. Returns `{ dispose }`. When `deps.createTerminal` is omitted the real
 * xterm is loaded asynchronously; tests always inject a mock for deterministic, synchronous setup.
 */
export function mountTerminalView(
  root: HTMLElement,
  transport: TerminalTransport,
  deps: TerminalViewDeps = {},
): { dispose(): void } {
  let term: TerminalLike | null = null
  let disposed = false

  const send = (bytes: Uint8Array): void => {
    try {
      transport.send(bytes)
    } catch {
      // Socket not open / torn down — drop; onClose surfaces the reason to the UI.
    }
  }

  const sendResize = (): void => {
    if (!term || !hasRealDimensions(root)) return
    term.fit()
    send(encodeClientMessage({ type: 'resize', cols: term.cols, rows: term.rows }))
  }

  const wire = (t: TerminalLike): void => {
    if (disposed) {
      t.dispose()
      return
    }
    term = t
    t.open(root)

    // keypress → xterm onData → transport.send (Enter → '\r' comes from xterm itself).
    t.onData((data) => send(encodeClientMessage({ type: 'input', data })))

    // incoming DATA → decode base ServerMessage → xterm.write (output only; attached/exit handled).
    transport.onMessage((bytes) => {
      const msg = decodeServerMessage(bytes)
      if (msg && msg.type === 'output') t.write(msg.data)
    })

    // attach MUST be the first client message (base invariant); sessionId=null → fresh session.
    send(encodeClientMessage({ type: 'attach', sessionId: null }))

    if (hasRealDimensions(root)) sendResize()
  }

  const created = deps.createTerminal
  if (created) {
    wire(created())
  } else {
    void defaultTerminal().then(wire)
  }

  return {
    dispose(): void {
      disposed = true
      if (term) term.dispose()
      transport.close()
    },
  }
}
