import { EventEmitter } from "node:events";
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, isAbsolute } from "node:path";
import { spawn } from "node:child_process";

const MESSAGE_TYPE_PROCESS_UPDATE = "ad-process:update";
const PROCESS_SESSION_WIDGET_ID = "processes-session";
const LIVE_STATUSES = new Set(["running", "terminating", "terminate_timeout"]);
const DEFAULT_OUTPUT_TAIL_LINES = 100;
const DEFAULT_KILL_TIMEOUT_MS = 3000;
const FORCE_KILL_TIMEOUT_MS = 250;
const DEFAULT_SHELL_CANDIDATES = [
  "/bin/bash",
  "/usr/bin/bash",
  "/bin/zsh",
  "/usr/bin/zsh",
  "/bin/sh",
  "/usr/bin/sh",
  "/run/current-system/sw/bin/bash",
];

function now() {
  return Date.now();
}

function safeNotify(ctx, message, level = "info") {
  try {
    if (ctx?.hasUI && ctx.ui?.notify) {
      ctx.ui.notify(message, level);
    }
  } catch {
    // Ignore stale UI contexts.
  }
}

function safeSetWidget(ctx, key, lines, options = { placement: "aboveEditor" }) {
  try {
    if (ctx?.hasUI && ctx.ui?.setWidget) {
      ctx.ui.setWidget(key, lines && lines.length > 0 ? lines : undefined, options);
    }
  } catch {
    // Ignore stale UI contexts.
  }
}

function safeSendMessage(pi, message, options) {
  try {
    pi.sendMessage(message, options);
  } catch (error) {
    if (error instanceof Error && error.message.includes("stale")) {
      return;
    }
    throw error;
  }
}

function shellExists(shellPath) {
  return typeof shellPath === "string" && isAbsolute(shellPath) && existsSync(shellPath);
}

function resolveShellExecutable() {
  const configured = process.env.ORCHESTRA_PI_PROCESSES_SHELL_PATH?.trim();
  if (shellExists(configured)) {
    return configured;
  }
  for (const candidate of DEFAULT_SHELL_CANDIDATES) {
    if (shellExists(candidate)) {
      return candidate;
    }
  }
  throw new Error("Unable to resolve a shell executable for pi-processes.");
}

function formatDuration(startTime, endTime = null) {
  const elapsedMs = Math.max(0, (endTime ?? now()) - startTime);
  const totalSeconds = Math.floor(elapsedMs / 1000);
  if (totalSeconds < 60) {
    return `${totalSeconds}s`;
  }
  const minutes = Math.floor(totalSeconds / 60);
  if (minutes < 60) {
    const seconds = totalSeconds % 60;
    return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
  }
  const hours = Math.floor(minutes / 60);
  const remMinutes = minutes % 60;
  return remMinutes > 0 ? `${hours}h ${remMinutes}m` : `${hours}h`;
}

function summarizeCommand(command) {
  const normalized = String(command ?? "").replace(/\s+/g, " ").trim();
  if (normalized.length <= 72) {
    return normalized;
  }
  return `${normalized.slice(0, 69)}...`;
}

function parseJsonObject(value) {
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}

function uniqueStrings(values) {
  const seen = new Set();
  const results = [];
  for (const value of values) {
    if (!value || seen.has(value)) {
      continue;
    }
    seen.add(value);
    results.push(value);
  }
  return results;
}

function tailLines(filePath, maxLines) {
  try {
    const content = readFileSync(filePath, "utf8");
    const lines = content.replace(/\r\n/g, "\n").split("\n");
    if (lines.length > 0 && lines[lines.length - 1] === "") {
      lines.pop();
    }
    return lines.slice(-maxLines);
  } catch {
    return [];
  }
}

function resolveProcessStatusLabel(processInfo) {
  if (processInfo.status === "exited") {
    return processInfo.success ? "exit(0)" : `exit(${processInfo.exitCode ?? "?"})`;
  }
  return processInfo.status;
}

function sortProcesses(processes) {
  return [...processes].sort((left, right) => {
    const leftLive = LIVE_STATUSES.has(left.status) ? 0 : 1;
    const rightLive = LIVE_STATUSES.has(right.status) ? 0 : 1;
    if (leftLive !== rightLive) {
      return leftLive - rightLive;
    }
    const leftTime = LIVE_STATUSES.has(left.status)
      ? left.startTime
      : (left.endTime ?? left.startTime);
    const rightTime = LIVE_STATUSES.has(right.status)
      ? right.startTime
      : (right.endTime ?? right.startTime);
    return rightTime - leftTime;
  });
}

class ProcessManager {
  constructor() {
    this.processes = new Map();
    this.events = new EventEmitter();
    this.counter = 0;
    this.logDir = join(tmpdir(), `orchestra-pi-processes-${Date.now()}`);
    mkdirSync(this.logDir, { recursive: true });
  }

  onEvent(listener) {
    this.events.on("event", listener);
    return () => this.events.off("event", listener);
  }

  emit(event) {
    this.events.emit("event", event);
  }

  list() {
    return sortProcesses([...this.processes.values()].map((entry) => entry.info));
  }

  get(id) {
    return this.processes.get(id)?.info ?? null;
  }

  getManaged(id) {
    return this.processes.get(id) ?? null;
  }

  getLogFiles(id) {
    const managed = this.getManaged(id);
    if (!managed) {
      return null;
    }
    return {
      stdoutFile: managed.info.stdoutFile,
      stderrFile: managed.info.stderrFile,
    };
  }

  clearFinished() {
    const removableIds = [...this.processes.entries()]
      .filter(([, managed]) => !LIVE_STATUSES.has(managed.info.status))
      .map(([id]) => id);
    for (const id of removableIds) {
      this.processes.delete(id);
    }
    if (removableIds.length > 0) {
      this.emit({ type: "processes_changed" });
    }
    return removableIds.length;
  }

  start(name, command, cwd, options = {}) {
    const trimmedName = String(name ?? "").trim();
    const trimmedCommand = String(command ?? "").trim();
    const launchCwd = String(cwd ?? process.cwd() ?? ".").trim() || process.cwd() || ".";
    if (!trimmedName) {
      throw new Error("Process name is required.");
    }
    if (!trimmedCommand) {
      throw new Error("Process command is required.");
    }

    const watches = this.resolveLogWatches(options.logWatches);
    const id = `proc_${++this.counter}`;
    const stdoutFile = join(this.logDir, `${id}-stdout.log`);
    const stderrFile = join(this.logDir, `${id}-stderr.log`);
    writeFileSync(stdoutFile, "", "utf8");
    writeFileSync(stderrFile, "", "utf8");

    const shell = resolveShellExecutable();
    const child = spawn(shell, ["-lc", trimmedCommand], {
      cwd: launchCwd,
      env: process.env,
      stdio: ["pipe", "pipe", "pipe"],
      detached: true,
    });

    if (typeof child.unref === "function") {
      child.unref();
    }

    const info = {
      id,
      name: trimmedName,
      pid: child.pid ?? -1,
      command: trimmedCommand,
      cwd: launchCwd,
      startTime: now(),
      endTime: null,
      status: "running",
      exitCode: null,
      success: null,
      stdoutFile,
      stderrFile,
      alertOnSuccess: options.alertOnSuccess === true,
      alertOnFailure: options.alertOnFailure !== false,
      alertOnKill: options.alertOnKill === true,
    };

    let resolveExit = () => {};
    const exitPromise = new Promise((resolve) => {
      resolveExit = resolve;
    });
    const managed = {
      info,
      child,
      stdinClosed: false,
      lastSignalSent: null,
      stdoutBuffer: "",
      stderrBuffer: "",
      watches,
      exited: false,
      exitPromise,
      resolveExit,
    };

    this.processes.set(id, managed);

    const handleData = (streamName, bufferKey, data) => {
      const text = typeof data === "string" ? data : data.toString("utf8");
      const filePath = streamName === "stdout" ? stdoutFile : stderrFile;
      appendFileSync(filePath, text, "utf8");
      managed[bufferKey] += text;
      const segments = managed[bufferKey].split(/\r?\n/);
      managed[bufferKey] = segments.pop() ?? "";
      for (const line of segments) {
        this.matchWatches(managed, streamName, line);
      }
      this.emit({ type: "process_output_changed", id });
    };

    child.stdout?.on("data", (data) => handleData("stdout", "stdoutBuffer", data));
    child.stderr?.on("data", (data) => handleData("stderr", "stderrBuffer", data));

    child.on("error", (error) => {
      if (managed.exited) {
        return;
      }
      info.exitCode = -1;
      info.success = false;
      info.endTime = now();
      info.status = "exited";
      appendFileSync(stderrFile, `Spawn error: ${error instanceof Error ? error.message : String(error)}\n`, "utf8");
      managed.exited = true;
      managed.resolveExit();
      this.emit({ type: "process_ended", info: { ...info } });
      this.emit({ type: "processes_changed" });
    });

    child.on("exit", (code, signal) => {
      if (managed.exited) {
        return;
      }
      if (managed.stdoutBuffer) {
        appendFileSync(stdoutFile, `${managed.stdoutBuffer}\n`, "utf8");
        this.matchWatches(managed, "stdout", managed.stdoutBuffer);
        managed.stdoutBuffer = "";
      }
      if (managed.stderrBuffer) {
        appendFileSync(stderrFile, `${managed.stderrBuffer}\n`, "utf8");
        this.matchWatches(managed, "stderr", managed.stderrBuffer);
        managed.stderrBuffer = "";
      }
      info.endTime = now();
      info.exitCode = typeof code === "number" ? code : null;
      if (managed.lastSignalSent || signal) {
        info.status = "killed";
        info.success = false;
      } else {
        info.status = "exited";
        info.success = info.exitCode === 0;
      }
      managed.exited = true;
      managed.resolveExit();
      this.emit({ type: "process_output_changed", id });
      this.emit({ type: "process_ended", info: { ...info } });
      this.emit({ type: "processes_changed" });
    });

    this.emit({ type: "process_started", info: { ...info } });
    this.emit({ type: "processes_changed" });
    return { ...info };
  }

  resolveLogWatches(watches) {
    if (!Array.isArray(watches)) {
      return [];
    }
    return watches.map((watch, index) => {
      const pattern = String(watch?.pattern ?? "").trim();
      if (!pattern) {
        throw new Error(`Invalid logWatches[${index}].pattern`);
      }
      return {
        index,
        pattern,
        regex: new RegExp(pattern),
        stream: watch?.stream === "stdout" || watch?.stream === "stderr" ? watch.stream : "both",
        repeat: watch?.repeat === true,
        fired: false,
      };
    });
  }

  matchWatches(managed, source, line) {
    if (!line) {
      return;
    }
    for (const watch of managed.watches) {
      if (watch.stream !== "both" && watch.stream !== source) {
        continue;
      }
      if (!watch.repeat && watch.fired) {
        continue;
      }
      if (!watch.regex.test(line)) {
        continue;
      }
      watch.fired = true;
      this.emit({
        type: "process_watch_matched",
        match: {
          processId: managed.info.id,
          processName: managed.info.name,
          processCommand: managed.info.command,
          source,
          line,
          watch: {
            index: watch.index,
            pattern: watch.pattern,
            stream: watch.stream,
            repeat: watch.repeat,
          },
        },
      });
    }
  }

  async kill(id, { signal = "SIGTERM", timeoutMs = DEFAULT_KILL_TIMEOUT_MS } = {}) {
    const managed = this.getManaged(id);
    if (!managed) {
      return { ok: false, reason: "not_found" };
    }
    if (!LIVE_STATUSES.has(managed.info.status)) {
      return { ok: false, reason: "not_found", info: { ...managed.info } };
    }

    const processGroupId = managed.info.pid;
    if (!processGroupId || processGroupId <= 0) {
      return { ok: false, reason: "error", info: { ...managed.info } };
    }

    managed.lastSignalSent = signal;
    if (signal === "SIGTERM" && managed.info.status === "running") {
      managed.info.status = "terminating";
      this.emit({ type: "processes_changed" });
    }

    try {
      process.kill(-processGroupId, signal);
    } catch {
      try {
        process.kill(processGroupId, signal);
      } catch {
        return { ok: false, reason: "error", info: { ...managed.info } };
      }
    }

    if (signal === "SIGKILL") {
      await Promise.race([
        managed.exitPromise,
        new Promise((resolve) => setTimeout(resolve, timeoutMs || FORCE_KILL_TIMEOUT_MS)),
      ]);
      return { ok: true, info: { ...managed.info } };
    }

    let exited = false;
    await Promise.race([
      managed.exitPromise.then(() => {
        exited = true;
      }),
      new Promise((resolve) => setTimeout(resolve, timeoutMs)).then(() => {
        exited = false;
      }),
    ]);

    if (exited || !LIVE_STATUSES.has(managed.info.status)) {
      return { ok: true, info: { ...managed.info } };
    }

    managed.info.status = "terminate_timeout";
    this.emit({ type: "processes_changed" });
    return { ok: false, reason: "timeout", info: { ...managed.info } };
  }

  writeToStdin(id, input, { end = false } = {}) {
    const managed = this.getManaged(id);
    if (!managed) {
      return { ok: false, reason: "not_found" };
    }
    if (!LIVE_STATUSES.has(managed.info.status)) {
      return { ok: false, reason: "process_exited" };
    }
    if (managed.stdinClosed || !managed.child.stdin || managed.child.stdin.destroyed) {
      return { ok: false, reason: "stdin_closed" };
    }
    try {
      managed.child.stdin.write(String(input));
      if (end) {
        managed.child.stdin.end();
        managed.stdinClosed = true;
      }
      return { ok: true };
    } catch {
      return { ok: false, reason: "write_error" };
    }
  }

  getOutput(id, tailLineCount = DEFAULT_OUTPUT_TAIL_LINES) {
    const managed = this.getManaged(id);
    if (!managed) {
      return null;
    }
    return {
      stdout: tailLines(managed.info.stdoutFile, tailLineCount),
      stderr: tailLines(managed.info.stderrFile, tailLineCount),
      status: managed.info.status,
    };
  }

  async shutdownKillAll() {
    const live = this.list().filter((entry) => LIVE_STATUSES.has(entry.status));
    for (const entry of live) {
      try {
        await this.kill(entry.id, { signal: "SIGKILL", timeoutMs: FORCE_KILL_TIMEOUT_MS });
      } catch {
        // Best effort during shutdown.
      }
    }
  }

  cleanup() {
    try {
      rmSync(this.logDir, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors.
    }
  }
}

function buildSessionWidgetLines(processes) {
  const active = sortProcesses(processes).filter((processInfo) => LIVE_STATUSES.has(processInfo.status));
  if (active.length === 0) {
    return [];
  }
  const lines = [active.length === 1 ? "● Processes (1 active)" : `● Processes (${active.length} active)`];
  active.forEach((processInfo, index) => {
    const prefix = index === active.length - 1 ? "└─" : "├─";
    const detailPrefix = index === active.length - 1 ? "   " : "│  ";
    lines.push(
      `${prefix} ${processInfo.name} (${processInfo.id}) · ${resolveProcessStatusLabel(processInfo)} · pid ${processInfo.pid} · ${formatDuration(processInfo.startTime, processInfo.endTime)}`,
    );
    lines.push(`${detailPrefix}cwd: ${processInfo.cwd}`);
    lines.push(`${detailPrefix}cmd: ${summarizeCommand(processInfo.command)}`);
  });
  return lines;
}

function buildLifecycleTranscriptMessage(details, content) {
  const statusLabel = details.status === "killed"
    ? "Killed"
    : details.success
      ? "Completed"
      : `Exited (${details.exitCode ?? "?"})`;
  return [
    `### Process ${statusLabel}`,
    `- ${details.processName} (${details.processId})`,
    `- runtime: ${details.runtime}`,
    `- command: \`${details.command}\``,
    content?.trim() ? `- note: ${content.trim()}` : "",
  ].filter(Boolean).join("\n");
}

function buildWatchTranscriptMessage(details, content) {
  return [
    "### Process watch matched",
    `- ${details.processName} (${details.processId})`,
    `- stream: ${details.source}`,
    `- pattern: \`/${details.watch?.pattern ?? ""}/\``,
    `- line: ${details.line}`,
    content?.trim() ? `- note: ${content.trim()}` : "",
  ].filter(Boolean).join("\n");
}

function setupProcessTranscriptHooks(pi, manager) {
  const repeatTurnCooldownMs = 5000;
  const lastRepeatTurnAt = new Map();

  manager.onEvent((event) => {
    if (event.type === "process_ended") {
      const info = event.info;
      const triggerTurn =
        (info.status === "killed" && info.alertOnKill) ||
        (info.status === "exited" && info.success && info.alertOnSuccess) ||
        (info.status === "exited" && !info.success && info.alertOnFailure);
      const content = info.status === "killed"
        ? `Process '${info.name}' was terminated (${formatDuration(info.startTime, info.endTime)})`
        : info.success
          ? `Process '${info.name}' completed successfully (${formatDuration(info.startTime, info.endTime)})`
          : `Process '${info.name}' crashed with exit code ${info.exitCode ?? "?"} (${formatDuration(info.startTime, info.endTime)})`;
      safeSendMessage(
        pi,
        {
          customType: MESSAGE_TYPE_PROCESS_UPDATE,
          content,
          display: true,
          details: {
            kind: "lifecycle",
            processId: info.id,
            processName: info.name,
            command: info.command,
            status: info.status,
            exitCode: info.exitCode,
            success: info.success ?? false,
            runtime: formatDuration(info.startTime, info.endTime),
          },
        },
        { triggerTurn },
      );
      return;
    }

    if (event.type !== "process_watch_matched") {
      return;
    }

    const watch = event.match.watch;
    const repeatKey = `${event.match.processId}:${watch.index}`;
    let triggerTurn = true;
    if (watch.repeat) {
      const elapsed = now() - (lastRepeatTurnAt.get(repeatKey) ?? 0);
      triggerTurn = elapsed >= repeatTurnCooldownMs;
      if (triggerTurn) {
        lastRepeatTurnAt.set(repeatKey, now());
      }
    }

    safeSendMessage(
      pi,
      {
        customType: MESSAGE_TYPE_PROCESS_UPDATE,
        content: `Watch matched for '${event.match.processName}' (${event.match.processId}) [${event.match.source}] /${watch.pattern}/`,
        display: true,
        details: {
          kind: "watch_matched",
          processId: event.match.processId,
          processName: event.match.processName,
          command: event.match.processCommand,
          source: event.match.source,
          line: event.match.line,
          watch,
        },
      },
      { triggerTurn },
    );
  });
}

function setupProcessSessionWidget(pi, manager) {
  let activeCtx = null;
  const update = () => {
    safeSetWidget(activeCtx, PROCESS_SESSION_WIDGET_ID, buildSessionWidgetLines(manager.list()));
  };
  manager.onEvent((event) => {
    if (
      event.type === "process_started"
      || event.type === "process_ended"
      || event.type === "process_output_changed"
      || event.type === "processes_changed"
    ) {
      update();
    }
  });
  pi.on("session_start", async (_event, ctx) => {
    activeCtx = ctx;
    update();
  });
  pi.on("session_shutdown", async (_event, ctx) => {
    activeCtx = null;
    safeSetWidget(ctx, PROCESS_SESSION_WIDGET_ID, undefined);
  });
  return { update };
}

function normalizeAction(params) {
  const input = parseJsonObject(params) ?? {};
  return {
    action: String(input.action ?? "").trim().toLowerCase(),
    name: typeof input.name === "string" ? input.name : undefined,
    command: typeof input.command === "string" ? input.command : undefined,
    id: typeof input.id === "string" ? input.id : undefined,
    input: typeof input.input === "string" ? input.input : undefined,
    end: input.end === true,
    alertOnSuccess: input.alertOnSuccess === true,
    alertOnFailure: input.alertOnFailure !== false,
    alertOnKill: input.alertOnKill === true,
    logWatches: Array.isArray(input.logWatches) ? input.logWatches : undefined,
  };
}

function buildToolResult(action, success, message, extra = {}) {
  return {
    content: [{ type: "text", text: message }],
    details: {
      action,
      success,
      message,
      ...extra,
    },
  };
}

async function executeProcessAction(manager, params, ctx) {
  const normalized = normalizeAction(params);
  switch (normalized.action) {
    case "start": {
      if (!normalized.name) {
        return buildToolResult("start", false, "Missing required parameter: name");
      }
      if (!normalized.command) {
        return buildToolResult("start", false, "Missing required parameter: command");
      }
      const processInfo = manager.start(normalized.name, normalized.command, ctx.cwd, {
        alertOnSuccess: normalized.alertOnSuccess,
        alertOnFailure: normalized.alertOnFailure,
        alertOnKill: normalized.alertOnKill,
        logWatches: normalized.logWatches,
      });
      return buildToolResult(
        "start",
        true,
        `Started \"${processInfo.name}\" (${processInfo.id}, PID: ${processInfo.pid})\nLog files:\n  stdout: ${processInfo.stdoutFile}\n  stderr: ${processInfo.stderrFile}`,
        { process: processInfo },
      );
    }
    case "list": {
      const processes = manager.list();
      if (processes.length === 0) {
        return buildToolResult("list", true, "No background processes running", { processes: [] });
      }
      const lines = processes.map((processInfo) => `${processInfo.id} \"${processInfo.name}\": ${summarizeCommand(processInfo.command)} [${resolveProcessStatusLabel(processInfo)}] ${formatDuration(processInfo.startTime, processInfo.endTime)}`);
      return buildToolResult("list", true, `${processes.length} process(es):\n${lines.join("\n")}`, { processes });
    }
    case "output": {
      if (!normalized.id) {
        return buildToolResult("output", false, "Missing required parameter: id");
      }
      const processInfo = manager.get(normalized.id);
      if (!processInfo) {
        return buildToolResult("output", false, `Process not found: ${normalized.id}`);
      }
      const output = manager.getOutput(processInfo.id, DEFAULT_OUTPUT_TAIL_LINES);
      const logFiles = manager.getLogFiles(processInfo.id);
      const stdout = output?.stdout ?? [];
      const stderr = output?.stderr ?? [];
      const messageParts = [`\"${processInfo.name}\" (${processInfo.id}) [${resolveProcessStatusLabel(processInfo)}]: ${stdout.length} stdout lines, ${stderr.length} stderr lines`];
      if (stdout.length > 0) {
        messageParts.push("", "stdout:", ...stdout);
      }
      if (stderr.length > 0) {
        messageParts.push("", "stderr:", ...stderr);
      }
      if (logFiles) {
        messageParts.push("", `stdout file: ${logFiles.stdoutFile}`, `stderr file: ${logFiles.stderrFile}`);
      }
      return buildToolResult("output", true, messageParts.join("\n"), { output, logFiles });
    }
    case "logs": {
      if (!normalized.id) {
        return buildToolResult("logs", false, "Missing required parameter: id");
      }
      const processInfo = manager.get(normalized.id);
      if (!processInfo) {
        return buildToolResult("logs", false, `Process not found: ${normalized.id}`);
      }
      const logFiles = manager.getLogFiles(processInfo.id);
      if (!logFiles) {
        return buildToolResult("logs", false, `Could not resolve log files for ${processInfo.id}`);
      }
      return buildToolResult("logs", true, `Log files for \"${processInfo.name}\" (${processInfo.id}):\n  stdout: ${logFiles.stdoutFile}\n  stderr: ${logFiles.stderrFile}\n\nUse the read tool to inspect these files.`, { logFiles });
    }
    case "kill": {
      if (!normalized.id) {
        return buildToolResult("kill", false, "Missing required parameter: id");
      }
      const processInfo = manager.get(normalized.id);
      if (!processInfo) {
        return buildToolResult("kill", false, `Process not found: ${normalized.id}`);
      }
      const signal = processInfo.status === "terminate_timeout" ? "SIGKILL" : "SIGTERM";
      const result = await manager.kill(processInfo.id, {
        signal,
        timeoutMs: signal === "SIGKILL" ? FORCE_KILL_TIMEOUT_MS : DEFAULT_KILL_TIMEOUT_MS,
      });
      if (result.ok) {
        return buildToolResult("kill", true, signal === "SIGKILL" ? `Force killed \"${processInfo.name}\" (${processInfo.id})` : `Terminated \"${processInfo.name}\" (${processInfo.id})`);
      }
      if (result.reason === "timeout") {
        return buildToolResult("kill", false, `SIGTERM timed out for \"${processInfo.name}\" (${processInfo.id}). The process is still marked terminate_timeout and can be force killed.`);
      }
      return buildToolResult("kill", false, `Failed to terminate \"${processInfo.name}\" (${processInfo.id})`);
    }
    case "clear": {
      const cleared = manager.clearFinished();
      return buildToolResult("clear", true, cleared > 0 ? `Cleared ${cleared} finished process(es)` : "No finished processes to clear", { cleared });
    }
    case "write": {
      if (!normalized.id) {
        return buildToolResult("write", false, "Missing required parameter: id");
      }
      if (normalized.input === undefined) {
        return buildToolResult("write", false, "Missing required parameter: input");
      }
      const processInfo = manager.get(normalized.id);
      if (!processInfo) {
        return buildToolResult("write", false, `Process not found: ${normalized.id}`);
      }
      const result = manager.writeToStdin(processInfo.id, normalized.input, { end: normalized.end });
      if (!result.ok) {
        return buildToolResult("write", false, `Unable to write to \"${processInfo.name}\" (${processInfo.id}): ${result.reason}`);
      }
      return buildToolResult("write", true, `Wrote ${normalized.input.length} bytes to \"${processInfo.name}\" (${processInfo.id})${normalized.end ? " and closed stdin" : ""}`);
    }
    default:
      return buildToolResult(normalized.action || "unknown", false, `Unknown action: ${normalized.action || "<missing>"}`);
  }
}

function registerProcessCommands(pi, manager, widgetController) {
  pi.registerCommand("ps", {
    description: "Show a compact process summary in the transcript",
    handler: async (_args, _ctx) => {
      const lines = buildSessionWidgetLines(manager.list());
      if (lines.length === 0) {
        safeSendMessage(pi, { customType: MESSAGE_TYPE_PROCESS_UPDATE, content: "No active processes.", display: true, details: { kind: "summary", activeCount: 0 } });
        return;
      }
      safeSendMessage(pi, { customType: MESSAGE_TYPE_PROCESS_UPDATE, content: lines.join("\n"), display: true, details: { kind: "summary", activeCount: manager.list().filter((entry) => LIVE_STATUSES.has(entry.status)).length } });
    },
  });

  pi.registerCommand("ps:kill", {
    description: "Kill a running managed process by id",
    handler: async (args, ctx) => {
      const processId = String(args ?? "").trim();
      if (!processId) {
        safeNotify(ctx, "Specify a process id, for example /ps:kill proc_1", "warning");
        return;
      }
      const processInfo = manager.get(processId);
      if (!processInfo) {
        safeNotify(ctx, `Process not found: ${processId}`, "warning");
        return;
      }
      const signal = processInfo.status === "terminate_timeout" ? "SIGKILL" : "SIGTERM";
      const result = await manager.kill(processInfo.id, {
        signal,
        timeoutMs: signal === "SIGKILL" ? FORCE_KILL_TIMEOUT_MS : DEFAULT_KILL_TIMEOUT_MS,
      });
      if (result.ok) {
        safeNotify(ctx, signal === "SIGKILL" ? `Force killed ${processInfo.name}` : `Terminated ${processInfo.name}`, signal === "SIGKILL" ? "warning" : "info");
      } else if (result.reason === "timeout") {
        safeNotify(ctx, `${processInfo.name} did not exit after SIGTERM; it is now force-kill eligible.`, "warning");
      } else {
        safeNotify(ctx, `Failed to terminate ${processInfo.name}.`, "error");
      }
      widgetController.update();
    },
  });

  pi.registerCommand("ps:clear", {
    description: "Remove finished processes from the managed list",
    handler: async (_args, ctx) => {
      const cleared = manager.clearFinished();
      safeNotify(ctx, cleared > 0 ? `Cleared ${cleared} finished process(es).` : "No finished processes to clear.", "info");
      widgetController.update();
    },
  });
}

function registerProcessTool(pi, manager) {
  pi.registerTool({
    name: "process",
    label: "Process",
    description: "Manage long-running background processes. Use start/list/output/logs/kill/clear/write instead of shell backgrounding when a command should keep running while the conversation continues.",
    promptSnippet: "Manage background processes without blocking the conversation",
    promptGuidelines: [
      "Use the process tool for long-running commands such as dev servers, watchers, and local APIs instead of bash backgrounding.",
      "After starting a process, continue other work instead of waiting for it.",
      "Use output or logs only when you need recent process state.",
      "Kill and clear processes when they are no longer useful.",
    ],
    parameters: {
      type: "object",
      additionalProperties: false,
      properties: {
        action: {
          type: "string",
          enum: ["start", "list", "output", "logs", "kill", "clear", "write"],
          description: "Action to perform.",
        },
        name: {
          type: "string",
          description: "Friendly process name for start.",
        },
        command: {
          type: "string",
          description: "Shell command to run for start.",
        },
        id: {
          type: "string",
          description: "Managed process id for output/logs/kill/write.",
        },
        input: {
          type: "string",
          description: "Text to write to stdin for write.",
        },
        end: {
          type: "boolean",
          description: "Close stdin after write.",
        },
        alertOnSuccess: {
          type: "boolean",
          description: "Trigger an agent turn when the process exits successfully.",
        },
        alertOnFailure: {
          type: "boolean",
          description: "Trigger an agent turn when the process exits unsuccessfully. Defaults true.",
        },
        alertOnKill: {
          type: "boolean",
          description: "Trigger an agent turn when the process is externally killed.",
        },
        logWatches: {
          type: "array",
          items: {
            type: "object",
            additionalProperties: false,
            properties: {
              pattern: { type: "string" },
              stream: { type: "string", enum: ["stdout", "stderr", "both"] },
              repeat: { type: "boolean" },
            },
            required: ["pattern"],
          },
          description: "Regex watches that surface watch-match transcript events while the process is running.",
        },
      },
      required: ["action"],
    },
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      return executeProcessAction(manager, params, ctx);
    },
  });
}

export default async function orchestraBundledPiProcesses(pi) {
  if (process.platform === "win32") {
    pi.on("session_start", async (_event, ctx) => {
      safeNotify(ctx, "pi-processes is not available on Windows.", "warning");
    });
    return;
  }

  const manager = new ProcessManager();
  const widgetController = setupProcessSessionWidget(pi, manager);
  setupProcessTranscriptHooks(pi, manager);
  registerProcessCommands(pi, manager, widgetController);
  registerProcessTool(pi, manager);

  pi.on("session_shutdown", async (_event) => {
    await manager.shutdownKillAll();
    manager.cleanup();
  });
}
