/**
 * tool-detail — central enrichment of abracadabra MCP tool cards.
 *
 * Built-in Claude tools (Bash/Read/Edit/…) flow through the hook-bridge, which
 * attaches an expandable `detail` (command text, file path, diff) that the
 * cou-sh chat renders as a click-to-expand block. abracadabra's own MCP tools
 * are explicitly skipped by the hook-bridge and only ever emitted a bare
 * name+target pill — so their cards had nothing to expand ("no idea what docs
 * it sees or what the tree was").
 *
 * `wrapToolsWithDetail` patches `McpServer.tool` ONCE, before any tool is
 * registered, so every tool invocation records an expandable detail onto its
 * tool-history entry (via `server.recordToolDetail`). The detail leads with the
 * human-readable title + id of any document the call referenced (resolved from
 * the live tree), then the remaining arguments, then the text the tool returned
 * (e.g. the full document tree for `get_document_tree`). The pill itself is
 * still emitted by each tool's own `setActiveToolCall`; this only fills in the
 * expandable detail afterwards.
 */
import { toPlain } from "@abraca/dabra";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { AbracadabraMCPServer } from "./server.ts";

/** Per-field cap (chars) — keeps the awareness payload bounded. Mirrors the
 *  hook-bridge's DETAIL_MAX so built-in and MCP tool cards behave alike. */
const DETAIL_MAX = 4000;

/** Argument keys that hold a document id, in the order we surface them. */
const DOC_ID_KEYS = [
	"docId",
	"id",
	"rootId",
	"parentId",
	"newParentId",
	"uploadId",
] as const;

function cap(s: string): string {
	return s.length > DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n… (truncated)` : s;
}

function safeJson(value: unknown): string {
	try {
		return JSON.stringify(value);
	} catch {
		return String(value);
	}
}

/** Pull the joined text content out of a tool result, if any. */
function resultText(result: unknown): string {
	const content = (result as { content?: unknown })?.content;
	if (!Array.isArray(content)) return "";
	return content
		.filter((c) => c && (c as { type?: string }).type === "text")
		.map((c) => (c as { text?: string }).text ?? "")
		.join("\n")
		.trim();
}

/**
 * Build the expandable detail shown when a tool card is clicked:
 *   1. one `id — "Title"` line per document the args referenced (title resolved
 *      from the live tree when available),
 *   2. the remaining arguments as compact JSON,
 *   3. the text the tool returned (document tree, content, confirmation, …).
 * Rendered by cou-sh's ChatActionCard as a `kind: "text"` block.
 */
export function buildToolDetail(
	toolArgs: unknown,
	result: unknown,
	resolveLabel?: (id: string) => string | undefined,
): { kind: "text"; text: string } {
	const lines: string[] = [];
	const rest: Record<string, unknown> =
		toolArgs && typeof toolArgs === "object"
			? { ...(toolArgs as Record<string, unknown>) }
			: {};

	// Headline: each referenced doc as "id — title" (title when resolvable).
	for (const key of DOC_ID_KEYS) {
		const v = rest[key];
		if (typeof v === "string" && v.length > 0) {
			const label = resolveLabel?.(v);
			lines.push(label ? `${key}: ${v} — "${label}"` : `${key}: ${v}`);
			delete rest[key];
		}
	}

	// Remaining arguments (depth, type, label, meta, content, query, …).
	const restStr = Object.keys(rest).length > 0 ? safeJson(rest) : "";
	if (restStr && restStr !== "{}") lines.push(`args: ${restStr}`);
	if (lines.length === 0) lines.push("args: (no arguments)");

	const text = resultText(result);
	if (text) {
		lines.push("");
		lines.push(text);
	}
	return { kind: "text", text: cap(lines.join("\n")) };
}

/**
 * Monkey-patch `mcp.tool` so each registered tool's handler is wrapped to record
 * arguments + result detail after it resolves. Call once at startup, before the
 * register* functions run. Errors in detail-recording are swallowed — they must
 * never break the underlying tool.
 */
export function wrapToolsWithDetail(
	mcp: McpServer,
	server: AbracadabraMCPServer,
): void {
	// Resolve a document id to its current label from the live tree, if loaded.
	const resolveLabel = (id: string): string | undefined => {
		try {
			const raw = server.getTreeMap()?.get(id);
			if (!raw) return undefined;
			const label = (toPlain(raw) as { label?: unknown })?.label;
			return typeof label === "string" && label.length > 0 ? label : undefined;
		} catch {
			return undefined;
		}
	};

	const original = mcp.tool.bind(mcp) as (...args: unknown[]) => unknown;
	(mcp as { tool: unknown }).tool = (...args: unknown[]) => {
		const name = args[0];
		const cbIndex = args.length - 1;
		const cb = args[cbIndex];
		if (typeof name === "string" && typeof cb === "function") {
			const handler = cb as (...a: unknown[]) => unknown;
			args[cbIndex] = async (...handlerArgs: unknown[]) => {
				const result = await handler(...handlerArgs);
				try {
					server.recordToolDetail(
						name,
						buildToolDetail(handlerArgs[0], result, resolveLabel),
					);
				} catch {
					/* detail recording must never break a tool */
				}
				return result;
			};
		}
		return original(...args);
	};
}
