// SF — Exec (context-mode) tool registration.
//
// Exposes the `sf_exec`, `sf_exec_search`, `sf_resume`, and `kill_agent` tools over MCP.
// Opt-in: sf_exec 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 type { ExtensionAPI } from "@singularity-forge/pi-coding-agent";
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";

export function registerExecTools(pi: ExtensionAPI): void {
	pi.registerTool({
		name: "sf_exec",
		label: "Exec (Sandboxed)",
		description:
			"Run a short script (bash/node/python) in a subprocess. Full stdout/stderr persist to " +
			".sf/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
			"this instead of reading many files or emitting large tool outputs — e.g. have the script " +
			"count/grep/summarize and log the finding. Enabled by default; opt out via " +
			"preferences.context_mode.enabled=false.",
		promptSnippet:
			"Run a bash/node/python script in a sandbox; full output is saved to disk and only a digest returns",
		promptGuidelines: [
			"Prefer sf_exec 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: Awaited<ReturnType<typeof loadEffectiveSFPreferences>> | null =
				null;
			try {
				prefs = loadEffectiveSFPreferences();
			} catch (err) {
				logWarning(
					"tool",
					`sf_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`,
				);
			}
			return executeSfExec(params as Parameters<typeof executeSfExec>[0], {
				baseDir: process.cwd(),
				preferences: prefs?.preferences ?? null,
			});
		},
	});

	pi.registerTool({
		name: "sf_exec_search",
		label: "Search sf_exec History",
		description:
			"List prior sf_exec runs (most recent first) from .sf/exec/*.meta.json. Useful for " +
			"rediscovering the stdout_path of an earlier run without re-executing it. Read-only.",
		promptSnippet:
			"Search prior sf_exec 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 as Parameters<typeof executeExecSearch>[0],
				{
					baseDir: process.cwd(),
				},
			);
		},
	});

	pi.registerTool({
		name: "sf_resume",
		label: "Resume (Read Snapshot)",
		description:
			"Return the contents of .sf/last-snapshot.md — a ≤2 KB digest of top memories, recent " +
			"sf_exec runs, and active context, written automatically on session_before_compact. Use " +
			"this after compaction or session resume to re-orient quickly.",
		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_query or sf_exec_search for detail.",
		],
		parameters: Type.Object({}),
		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
			return executeResume(params as Parameters<typeof executeResume>[0], {
				baseDir: process.cwd(),
			});
		},
	});

	/**
	 * Reload the agent — snapshot state, restart, and resume the same session.
	 *
	 * In headless mode: 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 (MCP servers, tools) without losing the session.",
			"The supervisor will resume the same session automatically in headless mode.",
			"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
		},
	});
}
