/**
 * @file pty-run.js
 * @description Owns the tmux-backed run lifecycle for the dashboard's
 * terminal-run feature: Start (create-or-adopt), Resume (`--resume`), Kill,
 * and List. Unlike the old run-spawner.js, there is no in-memory handle Map —
 * `listRuns`/`getRun` are computed fresh from `tmux list-sessions` on every
 * call, the same "computed fact, never a stored one" principle this repo
 * already applies to lane runtime up/down (see CLAUDE.md). A session killed
 * out-of-band (crash, manual `tmux kill-session`, host reboot) self-corrects
 * on the next read instead of leaving a ghost "running" row.
 *
 * Start/Resume is create-or-reuse: when the lane's tmux session already
 * exists but its pane sits at a shell prompt, the argv is typed into that
 * pane instead of being dropped on the floor by a silent adopt.
 *
 * Every session is named `ccam-lane-<laneId>` so a real terminal can attach
 * to the exact same session (`tmux attach -t ccam-lane-<id>`, or
 * `ccam lanes shell`) — that's the whole point: the dashboard both creates
 * the session (one-click Start/Resume) and is just one of possibly several
 * attached clients tmux already keeps in sync.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

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

let dashboardRuns = null;
try {
  dashboardRuns = require("./dashboard-runs");
} catch {
  /* db-less environment, skip persistence */
}

const RUN_ID_RE = /^ccam-lane-(\d+)$/;
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
const ALLOWED_PERMISSION_MODES = new Set(["acceptEdits", "default", "plan", "bypassPermissions"]);
// Pane commands that mean "idle shell prompt, safe to type a command into".
const SHELL_COMMANDS = new Set(["sh", "bash", "zsh", "fish", "dash", "ksh", "csh", "tcsh"]);

function runIdForLane(laneId) {
  return `ccam-lane-${laneId}`;
}

function laneIdFromRunId(id) {
  const m = typeof id === "string" ? id.match(RUN_ID_RE) : null;
  return m ? Number(m[1]) : null;
}

function makeErr(code, message) {
  const err = new Error(message);
  err.code = code;
  return err;
}

/**
 * Build the pane's command argv. Unlike the old stream-json spawner there is
 * no headless/conversation split — every run is a live interactive pane, so
 * an initial prompt (when given) is a trailing POSITIONAL argument: `claude`
 * treats a bare positional as the first turn's message and stays interactive
 * afterward (unlike `-p`, which forces print-and-exit and closes stdin).
 */
function buildArgv({ model, permissionMode, effort, resumeSessionId, initialPrompt }) {
  const argv = ["claude"];
  argv.push("--permission-mode", permissionMode || "acceptEdits");
  // Puts "bypass permissions" INTO the pane's shift+tab cycle without turning
  // it on: `claude --help` — "Enable bypassing all permission checks as an
  // option, without it being enabled by default." Without it the cycle is only
  // plan/auto/manual/acceptEdits, so a run started in any non-bypass mode could
  // never reach bypass from inside the pane, while a real terminal could.
  // The starting mode is still whatever --permission-mode says above.
  argv.push("--allow-dangerously-skip-permissions");
  if (model) argv.push("--model", model);
  if (effort && EFFORT_LEVELS.has(effort)) argv.push("--effort", effort);
  if (resumeSessionId) argv.push("--resume", resumeSessionId);
  if (initialPrompt) argv.push(initialPrompt);
  return argv;
}

/**
 * @param {object} args
 * @param {number} args.laneId
 * @param {string} args.cwd
 * @param {string} [args.model]
 * @param {string} [args.permissionMode]
 * @param {string} [args.effort]
 * @param {string} [args.resumeSessionId]
 * @param {string} [args.initialPrompt]
 */
function spawnRun(args) {
  const { laneId, cwd, model, permissionMode, effort, resumeSessionId, initialPrompt } = args || {};
  if (typeof laneId !== "number" || !Number.isInteger(laneId)) {
    throw makeErr("EBADLANE", "laneId must be an integer");
  }
  if (typeof cwd !== "string" || !cwd) {
    throw makeErr("EBADCWD", "cwd is required");
  }
  if (permissionMode != null && !ALLOWED_PERMISSION_MODES.has(permissionMode)) {
    throw makeErr(
      "EBADMODE",
      `permissionMode must be one of: ${Array.from(ALLOWED_PERMISSION_MODES).join(", ")}`
    );
  }
  if (effort != null && effort !== "" && !EFFORT_LEVELS.has(effort)) {
    throw makeErr("EBADEFFORT", `effort must be one of: ${Array.from(EFFORT_LEVELS).join(", ")}`);
  }
  if (
    resumeSessionId != null &&
    (typeof resumeSessionId !== "string" || !/^[A-Za-z0-9-]{8,}$/.test(resumeSessionId))
  ) {
    throw makeErr("EBADSESSION", "resumeSessionId is not a valid session id");
  }

  const id = runIdForLane(laneId);
  const startedAt = Date.now();
  const argv = buildArgv({ model, permissionMode, effort, resumeSessionId, initialPrompt });

  const record = () => {
    if (!dashboardRuns) return;
    dashboardRuns.recordRun({
      id,
      sessionId: resumeSessionId || null,
      mode: null,
      cwd,
      model: model || null,
      permissionMode: permissionMode || "acceptEdits",
      effort: effort || null,
      resumeSessionId: resumeSessionId || null,
      prompt: initialPrompt || "",
      status: "running",
      startedAt,
      endedAt: null,
      exitCode: null,
      laneId,
    });
  };

  if (!tmux.hasSession(id)) {
    tmux.newSession({ name: id, cwd, argv });
    record();
  } else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
    // The session exists but its pane is sitting at a bare shell prompt — a
    // `ccam lanes shell`, or a `claude` that already exited. Adopting it
    // silently here would swallow the whole request: a Resume would spawn no
    // `--resume` and an initial prompt would never be typed, while the API
    // still answered 200. Run the argv in the pane the user already sees
    // instead of erroring or opening a second session.
    tmux.sendCommand(id, argv);
    record();
  }
  // Pane is running something (a live `claude`, an editor, a build): adopt
  // silently, same convention as this repo's server port-adoption logic — no
  // error, no duplicate session. Attaching shows the user what is running.

  return getRun(id);
}

function killRun(id) {
  if (!id || !tmux.hasSession(id)) return false;
  tmux.killSession(id);
  if (dashboardRuns) {
    dashboardRuns.patchRun({ id, status: "killed", endedAt: Date.now() });
  }
  return true;
}

function publicRun(id) {
  const laneId = laneIdFromRunId(id);
  const live = tmux.hasSession(id);
  const row = dashboardRuns ? dashboardRuns.getRun(id) : null;
  return {
    id,
    laneId,
    status: live ? "running" : "gone",
    cwd: row?.cwd || null,
    model: row?.model || null,
    permissionMode: row?.permission_mode || null,
    effort: row?.effort || null,
    resumeSessionId: row?.resume_session_id || null,
    sessionId: row?.session_id || null,
    startedAt: row?.started_at || null,
    promptPreview: row?.prompt_preview || null,
  };
}

function getRun(id) {
  if (!id) return null;
  return publicRun(id);
}

/** Computed fresh from tmux state every call — see file header. */
function listRuns() {
  return tmux.listSessions("ccam-lane-").map(publicRun);
}

module.exports = {
  spawnRun,
  killRun,
  getRun,
  listRuns,
  laneIdFromRunId,
  runIdForLane,
};
