/**
 * tmux bridge for browser attach. Deliberately PTY-free: the collector renders a
 * pane snapshot (`capture-pane`) and injects keystrokes (`send-keys`) instead of
 * attaching a tmux client. Attaching a second client would resize the session to
 * the smaller of the two clients and shrink the terminal the user is really sitting
 * at; a snapshot never touches the session geometry.
 */

export interface TmuxCommandResult {
  rc: number;
  stdout: string;
  stderr: string;
}

export type TmuxSpawn = (argv: string[]) => Promise<TmuxCommandResult>;

export interface TmuxTarget {
  socket: string;
  session: string;
}

const COMMAND_TIMEOUT_MS = 5_000;

/** Keys that cannot be expressed as literal text; tmux key names, allowlisted. */
export const TMUX_NAMED_KEYS = [
  "Enter",
  "Escape",
  "Up",
  "Down",
  "Left",
  "Right",
  "BSpace",
  "Tab",
  "C-c",
  "C-d",
  "C-l",
] as const;
export type TmuxNamedKey = (typeof TMUX_NAMED_KEYS)[number];

export function tmuxBinary(): string | null {
  return Bun.which("tmux");
}

export async function defaultTmuxSpawn(argv: string[]): Promise<TmuxCommandResult> {
  const proc = Bun.spawn(argv, { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
  const timer = setTimeout(() => proc.kill(), COMMAND_TIMEOUT_MS);
  try {
    const [stdout, stderr, rc] = await Promise.all([
      new Response(proc.stdout).text(),
      new Response(proc.stderr).text(),
      proc.exited,
    ]);
    return { rc, stdout, stderr: stderr.trim() };
  } finally {
    clearTimeout(timer);
  }
}

/** `=name` pins an exact session match — an unanchored target is a glob pattern. */
function targetArg(target: TmuxTarget): string {
  return `=${target.session}`;
}

function baseArgv(binary: string, target: TmuxTarget): string[] {
  return [binary, "-S", target.socket];
}

export interface CaptureResult {
  ok: boolean;
  screen: string;
  error?: string;
}

/**
 * Snapshot of the pane's visible screen, SGR escapes preserved (`-e`) so colour
 * survives to the browser. Not the scrollback — the live screen is what an operator
 * checking on a wedged run needs.
 */
export async function captureScreen(
  target: TmuxTarget,
  deps: { binary: string; spawn?: TmuxSpawn },
): Promise<CaptureResult> {
  const spawn = deps.spawn ?? defaultTmuxSpawn;
  const result = await spawn([
    ...baseArgv(deps.binary, target),
    "capture-pane",
    "-p",
    "-e",
    "-t",
    targetArg(target),
  ]);
  if (result.rc !== 0) {
    return { ok: false, screen: "", error: result.stderr || `tmux capture-pane exited ${result.rc}` };
  }
  return { ok: true, screen: result.stdout };
}

export interface SendKeysInput {
  /** Literal text typed into the pane. */
  text?: string;
  /** One allowlisted tmux key name. */
  key?: TmuxNamedKey;
}

export async function sendKeys(
  target: TmuxTarget,
  input: SendKeysInput,
  deps: { binary: string; spawn?: TmuxSpawn },
): Promise<TmuxCommandResult> {
  const spawn = deps.spawn ?? defaultTmuxSpawn;
  const argv = [...baseArgv(deps.binary, target), "send-keys", "-t", targetArg(target)];
  if (input.text !== undefined) argv.push("-l", "--", input.text);
  else if (input.key !== undefined) argv.push("--", input.key);
  else return { rc: 2, stdout: "", stderr: "send-keys requires text or key" };
  return spawn(argv);
}
