// SF — Exec (context-mode) tool registration.
//
// Registers the `run_command`, `read_output`, `resume_agent`, and `kill_agent`
// tools as native SF agent tools.
// Opt-in: run_command is disabled unless `context_mode.enabled: true` is set
// (or left unset — enabled by default).
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { Type } from "@sinclair/typebox";
import { loadEffectiveSFPreferences } from "../preferences.js";

// Headless exit code for "reload with session resume".  Correlates with
// EXIT_RELOAD in src/headless-events.ts — kept in sync manually.
const EXIT_RELOAD = 12;

import { executeExecSearch } from "../tools/exec-search-tool.js";
import { executeSfExec } from "../tools/exec-tool.js";
import { executeResume } from "../tools/resume-tool.js";
import { logWarning } from "../workflow-logger.js";
import { getErrorMessage } from "../error-utils.js";
export function registerExecTools(pi) {
	pi.registerTool({
		name: "run_command",
		label: "Exec (Sandboxed)",
		description:
			"Run a shell command (bash, node, or python) in a subprocess and return a short digest of the output. " +
			"Full stdout/stderr are saved to disk and retrievable with read_output. " +
			"Use this for any shell operation — builds, tests, grep, git — whenever the raw output would be too large to return inline.",
		promptSnippet:
			"Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
		promptGuidelines: [
			"Prefer run_command for analyses that would otherwise read >3 files or produce large tool output.",
			"Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
			"The digest is the last ~300 chars of stdout — size your log output accordingly.",
			"Need the full output? Read the stdout_path returned in details (file on local disk).",
		],
		parameters: Type.Object({
			runtime: Type.Union(
				[
					Type.Literal("bash"),
					Type.Literal("node"),
					Type.Literal("python"),
					Type.Literal("python3"),
				],
				{ description: "Interpreter: bash (-c), node (-e), or python3 (-c)." },
			),
			script: Type.String({
				description:
					"Script body. Keep output small (log the finding, not the data).",
			}),
			purpose: Type.Optional(
				Type.String({
					description: "Short label recorded in meta.json for later review.",
				}),
			),
			timeout_ms: Type.Optional(
				Type.Number({
					description:
						"Per-invocation timeout (ms). Capped at 600000. Default from preferences.",
					minimum: 1_000,
					maximum: 600_000,
				}),
			),
		}),
		async execute(_toolCallId, params, _signal, onUpdate, _ctx) {
			let prefs = null;
			try {
				prefs = loadEffectiveSFPreferences();
			} catch (err) {
				logWarning(
					"tool",
					`run_command could not load preferences: ${getErrorMessage(err)}`,
				);
			}
			onUpdate?.({
				content: [
					{
						type: "text",
						text: `⏳ run_command: running ${params.runtime} script…`,
					},
				],
				details: { operation: "run_command", status: "running" },
			});
			return executeSfExec(params, {
				baseDir: process.cwd(),
				preferences: prefs?.preferences ?? null,
			});
		},
	});
	pi.registerTool({
		name: "read_output",
		label: "Search run_command History",
		description:
			"List recent run_command executions and retrieve their saved stdout/stderr. " +
			"Returns run metadata (command, exit code, output paths) ordered most-recent first. " +
			"Use this to re-read a prior command's full output without re-running it.",
		promptSnippet:
			"Search prior run_command runs by substring, runtime, or failing-only filter",
		promptGuidelines: [
			"Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
			"The preview shows the trailing ~300 chars of stdout; read stdout_path for the full transcript.",
		],
		parameters: Type.Object({
			query: Type.Optional(
				Type.String({
					description:
						"Substring matched against id and purpose (case-insensitive).",
				}),
			),
			runtime: Type.Optional(
				Type.Union(
					[
						Type.Literal("bash"),
						Type.Literal("node"),
						Type.Literal("python"),
						Type.Literal("python3"),
					],
					{
						description: "Restrict to one runtime.",
					},
				),
			),
			failing_only: Type.Optional(
				Type.Boolean({ description: "Only non-zero exit codes and timeouts." }),
			),
			limit: Type.Optional(
				Type.Number({
					description: "Max results (default 20, cap 200)",
					minimum: 1,
					maximum: 200,
				}),
			),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
			return executeExecSearch(params, {
				baseDir: process.cwd(),
			});
		},
	});
	pi.registerTool({
		name: "resume_agent",
		label: "Resume (Read Snapshot)",
		description:
			"Read the saved session snapshot and return a compact summary of recent commands, top memories, and active context. " +
			"Use this immediately after context compaction or session resume to re-orient before continuing work.",
		promptSnippet:
			"Read the pre-compaction snapshot to re-orient after context loss",
		promptGuidelines: [
			"Call this right after a session resumes if you feel you've lost durable context.",
			"The snapshot is a summary — use memory_search or read_output for detail.",
		],
		parameters: Type.Object({}),
		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
			return executeResume(params, {
				baseDir: process.cwd(),
			});
		},
	});
	/**
	 * Reload the agent — snapshot state, restart, and resume the same session.
	 *
	 * In the machine surface: writes sessionId to a sentinel file and exits with EXIT_RELOAD.
	 * The supervisor detects EXIT_RELOAD, reads the sessionId, and restarts with --resume.
	 * The agent resumes the same session with fresh extension code.
	 *
	 * In interactive TUI: exits the process (no session resume possible in TUI).
	 *
	 * Use after updating extension config files (e.g. ~/.mcp.json, ~/.sf/mcp.json)
	 * that require a process restart to take effect.
	 */
	pi.registerTool({
		name: "kill_agent",
		label: "Reload Agent (Snapshot & Resume)",
		description:
			"Snapshot the current session, kill the agent, and restart it resuming the same session. " +
			"Use after updating extension config files (e.g. ~/.mcp.json) that require a process restart. " +
			"The supervisor resumes the same session — agent continues from where it left off. " +
			"In interactive TUI: exits without session resume.",
		promptSnippet:
			"Snapshot and reload the pi-agent so it resumes the same session with fresh extension code",
		promptGuidelines: [
			"Use this to reload extension code, external MCP server config, or native tools without losing the session.",
			"The machine-surface supervisor will resume the same session automatically.",
			"In interactive TUI: the process exits and you restart manually.",
		],
		parameters: Type.Object({}),
		async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
			const tmpDir = process.env.TEMP ?? "/tmp";
			const sessionIdFile = join(tmpDir, "sf-current-session");
			const sentinelFile = join(tmpDir, "sf-reload-sentinel");
			// Read sessionId and write sentinel so runHeadless can resume this session
			if (existsSync(sessionIdFile)) {
				try {
					const sessionId = readFileSync(sessionIdFile, "utf-8").trim();
					if (sessionId) {
						writeFileSync(sentinelFile, sessionId, "utf-8");
					}
				} catch {
					// Fall through — exit with EXIT_RELOAD even without sessionId
				}
			}
			// Clear the session file so stale entries don't persist across reloads
			try {
				unlinkSync(sessionIdFile);
			} catch {
				// non-fatal
			}
			// EXIT_RELOAD (12) tells runHeadless to resume the session.
			// Falls back to normal restart if sentinel was not written.
			process.exit(EXIT_RELOAD);
			// unreachable
		},
	});
}
