/**
 * @file pty-attach.js
 * @description Bridges one WebSocket connection to an `@lydell/node-pty`-backed
 * `tmux attach-session` process. Binary WS frames carry raw PTY bytes in
 * both directions; text WS frames carry small JSON control messages
 * (`resize`, and an outbound `exit` sent once when the pane process/tmux
 * session ends). Multiple browser tabs each get their own PTY attach
 * process — tmux itself is what keeps them all in sync, this module does no
 * cross-connection coordination.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const tmux = require("./tmux");

const RUN_ID_RE = /^ccam-lane-\d+$/;

/**
 * Reject anything that isn't exactly `ccam-lane-<digits>` before it can ever
 * reach a tmux/PTY command — the trust boundary for this WS path, since a
 * validated runId is the only thing standing between an authenticated WS
 * client and naming an arbitrary session on the host.
 */
function validateRunId(runId) {
  if (typeof runId !== "string" || !RUN_ID_RE.test(runId)) {
    const err = new Error(`EBADRUNID: invalid runId: ${runId}`);
    err.code = "EBADRUNID";
    throw err;
  }
}

// Test seam — real implementation set in Step 4.
let spawnImpl = null;
function __setSpawnImpl(fn) {
  spawnImpl = fn;
}

/**
 * Attach `ws` to the tmux session `runId`. Spawns one PTY-backed
 * `tmux attach-session -t <runId>` per call.
 */
function attach(ws, runId, { cols, rows }) {
  validateRunId(runId);
  // Retroactively covers sessions started before mouse mode was wired into
  // newSession (or before a server restart picked that change up) — see
  // tmux.js's enableMouse.
  try {
    tmux.enableMouse(runId);
  } catch {
    /* session gone or tmux unavailable — attach below will surface that */
  }
  const pty = spawnImpl("tmux", ["attach-session", "-t", runId], {
    name: "xterm-256color",
    cols: cols || 80,
    rows: rows || 24,
  });

  pty.onData((data) => {
    try {
      ws.send(data, { binary: true });
    } catch {
      /* client gone between data event and send — safe to ignore */
    }
  });

  pty.onExit(({ exitCode }) => {
    try {
      ws.send(JSON.stringify({ type: "exit", code: exitCode }), { binary: false });
    } catch {
      /* ignore */
    }
    try {
      ws.close();
    } catch {
      /* ignore */
    }
  });

  ws.on("message", (data, isBinary) => {
    // node-pty attach processes for `tmux attach` are already tolerant of
    // resize mid-stream; the `ws` lib passes isBinary either as the second
    // callback arg (newer) or via `data.binary` on some transports — this
    // helper's own tests exercise the `{binary}` option shape used above.
    const binary = typeof isBinary === "boolean" ? isBinary : !!(isBinary && isBinary.binary);
    const text = data.toString("utf8");
    if (binary) {
      pty.write(text);
      return;
    }
    // The browser's WebSocket API sends a JS string as a text frame, and
    // xterm.js's onData callback hands over plain strings — so every
    // keystroke arrives here as text, not binary. Only a JSON control frame
    // (matched by this same prefix check the client uses for output) is
    // NOT keystroke input; everything else must reach the pty or typing
    // does nothing.
    if (text.startsWith('{"type"')) {
      let msg;
      try {
        msg = JSON.parse(text);
      } catch {
        pty.write(text);
        return;
      }
      if (msg && msg.type === "resize" && Number.isFinite(msg.cols) && Number.isFinite(msg.rows)) {
        pty.resize(msg.cols, msg.rows);
        return;
      }
    }
    pty.write(text);
  });

  ws.on("close", () => {
    try {
      pty.kill();
    } catch {
      /* already gone */
    }
  });

  return pty;
}

// Real spawn implementation — lazy-required so unit tests never load the
// native PTY addon unless they explicitly opt in. Uses @lydell/node-pty (a
// drop-in-API-compatible fork of node-pty) rather than node-pty itself:
// node-pty ships prebuilt binaries for darwin/win32 only, so on Linux it
// needs a native build via its install script — but the plugin install path
// runs `npm install --ignore-scripts` deliberately (see plugin-bootstrap.js)
// to avoid requiring a build toolchain on the user's machine. @lydell/node-pty
// instead ships the platform binary as a regular optionalDependency
// (@lydell/node-pty-linux-x64 etc.), so a plain --ignore-scripts install still
// resolves a working native binding with no compiler needed.
__setSpawnImpl((...args) => require("@lydell/node-pty").spawn(...args));

module.exports = { attach, validateRunId, __setSpawnImpl };
