/**
 * src/session/tmux.ts (H1) — thin synchronous wrappers around the tmux CLI.
 *
 * Used only when cfg.useTmux is on. Running the shell inside a tmux session
 * lets it survive a server/host restart: the node-pty process is just a tmux
 * *client*; killing it detaches, and the tmux server keeps the shell alive.
 *
 * Every call is best-effort and never throws (tmux missing / session gone are
 * treated as "false"/no-op).
 */

import { execFile, execFileSync } from 'node:child_process'
import { promisify } from 'node:util'
import { SESSION_ID_RE } from '../protocol.js'

const execFileAsync = promisify(execFile)

/** Upper bound on one `capture-pane` payload. A pane is one screen, so this is
 *  generous; it exists so a wedged tmux can never hand us unbounded output. */
const CAPTURE_MAX_BUFFER = 512 * 1024

/** Every tmux call is bounded. maxBuffer caps a flood but nothing caps the WAIT,
 *  and a tmux server stopped mid-syscall (SIGSTOP, a hung filesystem) would
 *  otherwise block forever — fatally so for the synchronous calls, which the event
 *  loop cannot interrupt. */
const TMUX_TIMEOUT_MS = 3000

/**
 * Tab-separated, machine-readable. tmux reports both clocks in unix SECONDS.
 *
 * The activity clock is `window_activity`, NOT `session_activity`. That is not a
 * detail: `session_activity` tracks CLIENT activity — it advances on attach and on
 * keypresses — and never moves for a detached session no matter how much its shell
 * is printing. Measured on tmux 3.6a with a detached session in a `while true; do
 * echo tick; sleep 1; done` loop: after 6s, session_activity was still equal to
 * session_created while window_activity had advanced. Using session_activity as the
 * liveness signal would make the idle-cleanup kill precisely the long-running,
 * unattended, actively-working sessions this whole feature exists to protect.
 */
const LIST_FORMAT = [
  '#{session_name}',
  '#{session_created}',
  '#{window_activity}',
  '#{session_attached}',
  '#{window_width}',
  '#{window_height}',
].join('\t')

/**
 * One `web_*` tmux session as tmux itself describes it (A).
 *
 * `attached` is tmux's own client count, NOT "this server is streaming it": a
 * session can be attached from a plain terminal, or by a second web-terminal
 * process. So "we do not track it" and "nobody is using it" are different
 * questions, and the UI must not conflate them.
 */
export interface TmuxSessionSummary {
  readonly id: string
  readonly createdAtMs: number
  readonly lastActivityAtMs: number
  readonly attached: boolean
  readonly cols: number
  readonly rows: number
}

/** Is the tmux binary available on PATH? */
export function tmuxAvailable(): boolean {
  try {
    execFileSync('tmux', ['-V'], { stdio: 'ignore', timeout: TMUX_TIMEOUT_MS })
    return true
  } catch {
    return false
  }
}

/** tmux session name for a web-terminal session id (UUIDs are tmux-safe). */
export function tmuxName(sessionId: string): string {
  return `web_${sessionId}`
}

/** Does a tmux session with this name already exist (e.g. after a restart)? */
export function hasSession(name: string): boolean {
  try {
    execFileSync('tmux', ['has-session', '-t', name], {
      stdio: 'ignore',
      timeout: TMUX_TIMEOUT_MS,
    })
    return true
  } catch {
    return false
  }
}

/** Kill a tmux session (ends the shell). No-op if it's already gone. */
export function killSession(name: string): void {
  try {
    execFileSync('tmux', ['kill-session', '-t', name], {
      stdio: 'ignore',
      timeout: TMUX_TIMEOUT_MS,
    })
  } catch {
    // already gone — fine
  }
}

/**
 * Parse `tmux list-sessions -F LIST_FORMAT` output. Pure, so the parsing rules are
 * testable without a tmux binary.
 *
 * Two filters, both load-bearing:
 *  - the name must start with `web_`, so we never enumerate (and therefore never
 *    offer to kill) a tmux session the user created for their own work;
 *  - the remainder must be a UUID v4 per SESSION_ID_RE — the same gate the attach
 *    protocol applies (M7). Anything else cannot have been created by this app, and
 *    refusing it here is also what keeps a hostile name from reaching `tmux -t`
 *    (e.g. a literal `-t`, or a name full of path traversal).
 */
export function parseSessionList(stdout: string): TmuxSessionSummary[] {
  const rows: TmuxSessionSummary[] = []
  for (const line of stdout.split('\n')) {
    if (line === '') continue
    const parts = line.split('\t')
    if (parts.length < 6) continue

    const [name, created, activity, attached, width, height] = parts as [
      string,
      string,
      string,
      string,
      string,
      string,
    ]
    if (!name.startsWith('web_')) continue
    const id = name.slice('web_'.length)
    if (!SESSION_ID_RE.test(id)) continue

    const createdSec = Number(created)
    const activitySec = Number(activity)
    const cols = Number(width)
    const rows_ = Number(height)
    if (
      !Number.isFinite(createdSec) ||
      !Number.isFinite(activitySec) ||
      !Number.isFinite(cols) ||
      !Number.isFinite(rows_)
    ) {
      continue
    }

    rows.push({
      id,
      createdAtMs: createdSec * 1000,
      lastActivityAtMs: activitySec * 1000,
      attached: attached !== '0' && attached !== '',
      cols,
      rows: rows_,
    })
  }
  return rows.sort((a, b) => b.lastActivityAtMs - a.lastActivityAtMs)
}

/**
 * Every `web_*` tmux session on the host, most recently active first. Never
 * throws; [] when tmux is missing or no tmux server is running.
 *
 * ASYNC on purpose. This is polled — every device's home screen refreshes on a
 * timer — and the spawn costs ~73 ms on a host with 69 sessions. Done
 * synchronously that is 73 ms with the event loop stopped, i.e. 73 ms during which
 * no PTY byte reaches any terminal on any device. The server is a byte-shuttle
 * first; nothing on a polling path may block it.
 */
export async function listSessions(): Promise<TmuxSessionSummary[]> {
  try {
    const { stdout } = await execFileAsync('tmux', ['list-sessions', '-F', LIST_FORMAT], {
      encoding: 'utf8',
      maxBuffer: CAPTURE_MAX_BUFFER,
      timeout: TMUX_TIMEOUT_MS,
    })
    return parseSessionList(stdout)
  } catch {
    return []
  }
}

/**
 * The session's CURRENT screen, without attaching. Returns null if it is gone.
 *
 * `-p` prints to stdout and `-e` keeps the escape sequences, so the bytes can go
 * straight into a read-only xterm and keep their colour. Deliberately NOT
 * `attach-session`: attaching would make tmux resize the window to the new
 * client's dimensions and SIGWINCH whatever is running inside it.
 */
export async function capturePane(name: string): Promise<string | null> {
  try {
    const { stdout } = await execFileAsync('tmux', ['capture-pane', '-p', '-e', '-t', name], {
      encoding: 'utf8',
      maxBuffer: CAPTURE_MAX_BUFFER,
      timeout: TMUX_TIMEOUT_MS,
    })
    return stdout
  } catch {
    return null
  }
}
