/**
 * MCP Client Extension — client-side integration with external MCP tool servers
 *
 * Provides on-demand access to MCP servers configured in project files
 * (.mcp.json, .sf/mcp.json) using the @modelcontextprotocol/sdk Client
 * directly — no external CLI dependency required.
 *
 * Three tools:
 *   mcp_servers   — List available MCP servers from config files
 *   mcp_discover  — Get tool signatures for a specific server (lazy connect)
 *   mcp_call      — Call a tool on an MCP server (lazy connect)
 *
 * Connection logic lives in packages/coding-agent/src/core/mcp/.
 * This file is the thin extension wrapper: tool definitions + lifecycle hooks.
 */
import { Type } from "@sinclair/typebox";
import {
	DEFAULT_MAX_BYTES,
	DEFAULT_MAX_LINES,
	formatSize,
	McpConnectionManager,
	truncateHead,
} from "@singularity-forge/coding-agent";
import { Text } from "@singularity-forge/tui";

// ─── Module-level manager (session-scoped) ────────────────────────────────────
const manager = new McpConnectionManager();

// ─── JSON Schema → TypeBox converter ─────────────────────────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function jsonSchemaPropToTypeBox(schema) {
	if (!schema || typeof schema !== "object") return Type.Any();
	const t = schema.type;
	if (t === "string") return Type.String({ description: schema.description });
	if (t === "number" || t === "integer")
		return Type.Number({ description: schema.description });
	if (t === "boolean") return Type.Boolean({ description: schema.description });
	if (t === "array") return Type.Array(Type.Any());
	if (t === "object") {
		const props = schema.properties;
		if (props) {
			const entries = {};
			for (const [k, v] of Object.entries(props)) {
				entries[k] = jsonSchemaPropToTypeBox(v);
			}
			return Type.Object(entries);
		}
	}
	return Type.Any();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function jsonSchemaToTypeBox(schema) {
	if (!schema || typeof schema !== "object") return Type.Object({});
	const obj = schema;
	const props = obj.properties;
	if (!props) return Type.Object({});
	const entries = {};
	for (const [k, v] of Object.entries(props)) {
		entries[k] = jsonSchemaPropToTypeBox(v);
	}
	return Type.Object(entries);
}

// ─── Formatters ───────────────────────────────────────────────────────────────
function formatServerList(servers) {
	if (servers.length === 0)
		return "No MCP servers configured. Add servers to .mcp.json or .sf/mcp.json.";
	const lines = [`${servers.length} MCP servers configured:\n`];
	for (const s of servers) {
		const connected = manager.isConnected(s.name) ? "✓" : "○";
		const cached = manager.getCachedTools(s.name);
		const toolCount = cached ? ` — ${cached.length} tools` : "";
		lines.push(`${connected} ${s.name} (${s.transport})${toolCount}`);
	}
	lines.push(
		"\nUse mcp_discover to see full tool schemas for a specific server.",
	);
	lines.push("Use mcp_call to invoke a tool: mcp_call(server, tool, args).");
	return lines.join("\n");
}
function formatToolList(serverName, tools) {
	const lines = [`${serverName} — ${tools.length} tools:\n`];
	for (const tool of tools) {
		lines.push(`## ${tool.name}`);
		if (tool.description) lines.push(tool.description);
		if (tool.inputSchema) {
			lines.push("```json");
			lines.push(JSON.stringify(tool.inputSchema, null, 2));
			lines.push("```");
		}
		lines.push("");
	}
	lines.push(
		`Call with: mcp_call(server="${serverName}", tool="<tool_name>", args={...})`,
	);
	return lines.join("\n");
}

// ─── Status helpers (consumed by /sf mcp) ────────────────────────────────────
/**
 * Disconnect all active MCP connections and clear the tool cache.
 * Servers will lazily reconnect on the next mcp_discover or mcp_call.
 *
 * Purpose: allow /mcp reload to pick up config changes without a full restart.
 * Consumer: /mcp reload command handler in commands-mcp-status.js.
 */
export async function disconnectAll() {
	await manager.disconnectAll();
}

/**
 * Return the live connection status for a named MCP server.
 * Safe to call even when the server has never been connected.
 */
export function getConnectionStatus(name) {
	return manager.getConnectionStatus(name);
}

// ─── Test-exported helpers ────────────────────────────────────────────────────
export function _buildMcpChildEnvForTest(env) {
	return manager.buildChildEnv(env);
}
export function _buildMcpTrustConfirmOptionsForTest(signal) {
	return { timeout: 120_000, signal };
}

// ─── Extension ────────────────────────────────────────────────────────────────
export default function (pi) {
	// ── mcp_servers ──────────────────────────────────────────────────────────
	pi.registerTool({
		name: "mcp_servers",
		label: "MCP Servers",
		description:
			"List all available MCP servers configured in project files (.mcp.json, .sf/mcp.json). " +
			"Shows server names, transport type, and connection status. After mcp_discover, each server's " +
			"tools are auto-registered as first-class pi tools (e.g. serena_find_symbol).",
		promptSnippet: "List available MCP servers from project configuration",
		promptGuidelines: [
			"Call mcp_servers to see what MCP servers are available before trying to use one.",
			"After mcp_discover(server), the server's tools appear as real pi tools.",
			"MCP servers provide external integrations (Twitter, Linear, Railway, etc.) via the Model Context Protocol.",
			"After listing, use mcp_discover(server) to get tool schemas, then mcp_call(server, tool, args) to invoke.",
		],
		parameters: Type.Object({
			refresh: Type.Optional(
				Type.Boolean({
					description: "Force refresh the server list (default: use cache)",
				}),
			),
		}),
		async execute(_id, params) {
			if (params.refresh) manager.invalidateConfigCache();
			const servers = manager.readConfigs();
			return {
				content: [{ type: "text", text: formatServerList(servers) }],
				details: {
					serverCount: servers.length,
					cached: !params.refresh,
				},
			};
		},
		renderCall(args, theme) {
			let text = theme.fg("toolTitle", theme.bold("mcp_servers"));
			if (args.refresh) text += theme.fg("warning", " (refresh)");
			return new Text(text, 0, 0);
		},
		renderResult(result, { isPartial }, theme) {
			if (isPartial)
				return new Text(theme.fg("warning", "Reading MCP config..."), 0, 0);
			const d = result.details;
			return new Text(
				theme.fg("success", `${d?.serverCount ?? 0} servers configured`),
				0,
				0,
			);
		},
	});

	// ── mcp_discover ─────────────────────────────────────────────────────────
	pi.registerTool({
		name: "mcp_discover",
		label: "MCP Discover",
		description:
			"Get detailed tool signatures and JSON schemas for a specific MCP server. " +
			"Connects to the server on first call (lazy connection). " +
			"After discovery, each MCP tool is auto-registered as a first-class pi tool " +
			"(e.g. serena_find_symbol) — the LLM can call them directly without mcp_call.",
		promptSnippet:
			"Discover MCP server tools and register them as first-class pi tools",
		promptGuidelines: [
			"Call mcp_discover(server) to connect to an MCP server and surface its tools.",
			"After discovery, the LLM sees each tool by its real name (e.g. serena_search_for_pattern).",
			"Call tools directly by their names instead of going through mcp_call.",
		],
		parameters: Type.Object({
			server: Type.String({
				description:
					"MCP server name (from mcp_servers output), e.g. 'railway', 'twitter-mcp', 'linear'",
			}),
		}),
		async execute(_id, params, signal) {
			try {
				const cached = manager.getCachedTools(params.server);
				if (cached) {
					const text = formatToolList(params.server, cached);
					const truncation = truncateHead(text, {
						maxLines: DEFAULT_MAX_LINES,
						maxBytes: DEFAULT_MAX_BYTES,
					});
					let finalText = truncation.content;
					if (truncation.truncated) {
						finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
					}
					return {
						content: [{ type: "text", text: finalText }],
						details: {
							server: params.server,
							toolCount: cached.length,
							cached: true,
						},
					};
				}
				const client = await manager.getOrConnect(params.server, signal);
				const result = await client.listTools(undefined, {
					signal,
					timeout: 30000,
				});
				const tools = (result.tools ?? []).map((t) => ({
					name: t.name,
					description: t.description ?? "",
					inputSchema: t.inputSchema,
				}));
				manager.setCachedTools(params.server, tools);
				// Auto-register each MCP tool as a first-class pi tool.
				manager.registerToolsForServer(
					params.server,
					tools,
					({ name, label, description, inputSchemaRaw, execute }) => {
						const paramType = inputSchemaRaw
							? jsonSchemaToTypeBox(inputSchemaRaw)
							: Type.Object({});
						pi.registerTool({
							name,
							label,
							description,
							parameters: paramType,
							async execute(id, toolParams, toolSignal) {
								const res = await execute(id, toolParams, toolSignal);
								const truncation = truncateHead(res.content[0]?.text ?? "", {
									maxLines: DEFAULT_MAX_LINES,
									maxBytes: DEFAULT_MAX_BYTES,
								});
								let finalText = truncation.content;
								if (truncation.truncated) {
									finalText += `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines]`;
								}
								return {
									content: [{ type: "text", text: finalText }],
									details: res.details,
								};
							},
						});
					},
				);
				const text = formatToolList(params.server, tools);
				const truncation = truncateHead(text, {
					maxLines: DEFAULT_MAX_LINES,
					maxBytes: DEFAULT_MAX_BYTES,
				});
				let finalText = truncation.content;
				if (truncation.truncated) {
					finalText += `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
				}
				return {
					content: [{ type: "text", text: finalText }],
					details: {
						server: params.server,
						toolCount: tools.length,
						cached: false,
					},
				};
			} catch (err) {
				const msg = err instanceof Error ? err.message : String(err);
				throw new Error(
					`Failed to discover tools for "${params.server}": ${msg}`,
				);
			}
		},
		renderCall(args, theme) {
			let text = theme.fg("toolTitle", theme.bold("mcp_discover "));
			text += theme.fg("accent", args.server);
			return new Text(text, 0, 0);
		},
		renderResult(result, { isPartial }, theme) {
			if (isPartial)
				return new Text(theme.fg("warning", "Discovering tools..."), 0, 0);
			const d = result.details;
			return new Text(
				theme.fg("success", `${d?.toolCount ?? 0} tools`) +
					theme.fg("dim", ` · ${d?.server}`),
				0,
				0,
			);
		},
	});

	// ── mcp_call ─────────────────────────────────────────────────────────────
	pi.registerTool({
		name: "mcp_call",
		label: "MCP Call",
		description:
			"Call a tool on an MCP server. Provide the server name, tool name, and arguments. " +
			"Connects to the server on first call (lazy connection). " +
			"Use mcp_discover first to see available tools and their required arguments.",
		promptSnippet: "Call a tool on an MCP server",
		promptGuidelines: [
			"Always use mcp_discover first to understand the tool's parameters before calling mcp_call.",
			"Arguments are passed as a JSON object matching the tool's input schema.",
		],
		parameters: Type.Object({
			server: Type.String({
				description: "MCP server name, e.g. 'railway', 'twitter-mcp'",
			}),
			tool: Type.String({
				description: "Tool name on that server, e.g. 'railway_list_projects'",
			}),
			args: Type.Optional(
				Type.Object(
					{},
					{
						additionalProperties: true,
						description:
							"Tool arguments as key-value pairs matching the tool's input schema",
					},
				),
			),
		}),
		async execute(_id, params, signal) {
			try {
				const client = await manager.getOrConnect(params.server, signal);
				const result = await client.callTool(
					{ name: params.tool, arguments: params.args ?? {} },
					undefined,
					{ signal, timeout: 60000 },
				);
				const contentItems = result.content;
				const raw = contentItems
					.map((c) => (c.type === "text" ? (c.text ?? "") : JSON.stringify(c)))
					.join("\n");
				const truncation = truncateHead(raw, {
					maxLines: DEFAULT_MAX_LINES,
					maxBytes: DEFAULT_MAX_BYTES,
				});
				let finalText = truncation.content;
				if (truncation.truncated) {
					finalText += `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
				}
				return {
					content: [{ type: "text", text: finalText }],
					details: {
						server: params.server,
						tool: params.tool,
						charCount: finalText.length,
						truncated: truncation.truncated,
					},
				};
			} catch (err) {
				const msg = err instanceof Error ? err.message : String(err);
				throw new Error(
					`MCP call failed: ${params.server}.${params.tool}\n${msg}`,
				);
			}
		},
		renderCall(args, theme) {
			let text = theme.fg("toolTitle", theme.bold("mcp_call "));
			text += theme.fg("accent", `${args.server}.${args.tool}`);
			if (args.args && Object.keys(args.args).length > 0) {
				const preview = Object.entries(args.args)
					.slice(0, 3)
					.map(([k, v]) => {
						const val = typeof v === "string" ? v : JSON.stringify(v);
						return `${k}:${val.length > 30 ? val.slice(0, 30) + "…" : val}`;
					})
					.join(" ");
				text += " " + theme.fg("muted", preview);
			}
			return new Text(text, 0, 0);
		},
		renderResult(result, { isPartial, expanded }, theme) {
			if (isPartial)
				return new Text(theme.fg("warning", "Calling MCP tool..."), 0, 0);
			const d = result.details;
			let text = theme.fg("success", `✓ ${d?.server}.${d?.tool}`);
			text += theme.fg(
				"dim",
				` · ${(d?.charCount ?? 0).toLocaleString()} chars`,
			);
			if (d?.truncated) text += theme.fg("warning", " · truncated");
			if (expanded) {
				const content = result.content[0];
				if (content?.type === "text") {
					const preview = content.text.split("\n").slice(0, 15).join("\n");
					text += "\n\n" + theme.fg("dim", preview);
				}
			}
			return new Text(text, 0, 0);
		},
	});

	// ── Lifecycle ─────────────────────────────────────────────────────────────
	pi.on("session_start", async (_event, ctx) => {
		const servers = manager.readConfigs();
		if (servers.length > 0) {
			ctx.ui.notify(
				`MCP client ready — ${servers.length} server(s) configured`,
				"info",
			);
		}
	});
	pi.on("session_shutdown", async () => {
		await manager.closeAll();
	});
	pi.on("session_switch", async () => {
		await manager.closeAll();
		manager.invalidateConfigCache();
	});
}
