/**
 * Sift Search Tool — direct CLI wrapper exposing Sift's full retrieval surface.
 *
 * Purpose: give agents full control over Sift's hybrid BM25+vector pipeline,
 * including agentic multi-turn search, strategy selection, and planner config.
 * Complements the platform `codebase_search` tool by exposing Sift-specific
 * features (`--agent`, `--planner-strategy`, etc.) that the generic tool
 * cannot surface.
 *
 * Consumer: executing agents that need Sift's advanced retrieval modes.
 */
import { execFile } from "node:child_process";
import { Type } from "@sinclair/typebox";
import {
	buildSiftEnv,
	ensureSiftRuntimeDirs,
	resolveSiftBinary,
	resolveSiftSearchScope,
} from "../code-intelligence.js";
import { recordRetrievalEvidence } from "../retrieval-evidence.js";
import { getErrorMessage } from "../error-utils.js";

const _KNOWN_STRATEGIES = [
	"hybrid",
	"page-index-hybrid",
	"path-hybrid",
	"bm25",
	"vector",
	"page-index-llm",
	"page-index-jina",
	"page-index-gemma",
	"path-fuzzy",
	"segment-fuzzy",
];

const DEFAULT_STRATEGY = "page-index-hybrid";
const DEFAULT_LIMIT = 10;
const DEFAULT_RETRIEVER_TIMEOUT_MS = 30_000;
const DEFAULT_TIMEOUT_MS = 60_000;

/**
 * Build the sift CLI argument list from tool parameters.
 */
function buildSiftArgs(params, projectRoot = process.cwd()) {
	const scope = resolveSiftSearchScope(projectRoot, params.path);
	const args = [
		"search",
		"--json",
		"--strategy",
		String(params.strategy ?? DEFAULT_STRATEGY),
		"--limit",
		String(params.limit ?? DEFAULT_LIMIT),
		"--retriever-timeout-ms",
		String(params.retrieverTimeoutMs ?? DEFAULT_RETRIEVER_TIMEOUT_MS),
	];

	if (params.agent === true) {
		args.push("--agent");
		if (params.agentMode) {
			args.push("--agent-mode", String(params.agentMode));
		}
		if (params.plannerStrategy) {
			args.push("--planner-strategy", String(params.plannerStrategy));
		}
	}

	// Path and query are positional
	args.push(scope, params.query);
	return args;
}

/**
 * Parse the sift JSON output into a structured result.
 */
function parseSiftOutput(rawStdout, rawStderr) {
	let parsed;
	try {
		parsed = JSON.parse(rawStdout);
	} catch {
		return {
			hits: [],
			query: "",
			strategy: "",
			elapsedMs: 0,
			stderr: rawStderr,
			raw: rawStdout,
		};
	}

	// Sift JSON output shape varies by version; handle both array and object forms
	const hits = Array.isArray(parsed)
		? parsed
		: Array.isArray(parsed.hits)
			? parsed.hits
			: [];

	return {
		hits: hits.map((h) => ({
			path: h.path ?? h.file ?? "",
			score: h.score ?? h.rank ?? 0,
			content: h.content ?? h.snippet ?? "",
			lineStart: h.line_start ?? h.lineStart ?? null,
			lineEnd: h.line_end ?? h.lineEnd ?? null,
		})),
		query: parsed.query ?? "",
		strategy: parsed.strategy ?? "",
		elapsedMs: parsed.elapsed_ms ?? parsed.elapsedMs ?? 0,
		stderr: rawStderr,
	};
}

/**
 * Execute a sift search with the given parameters.
 */
function runSift(binaryPath, args, timeoutMs, projectRoot) {
	return new Promise((resolve, reject) => {
		ensureSiftRuntimeDirs(projectRoot);
		const _child = execFile(
			binaryPath,
			args,
			{
				cwd: projectRoot,
				encoding: "utf-8",
				env: buildSiftEnv(projectRoot, process.env),
				maxBuffer: 16 * 1024 * 1024,
				timeout: timeoutMs,
			},
			(error, stdout, stderr) => {
				if (error && !stdout) {
					reject(error);
					return;
				}
				resolve({ stdout, stderr });
			},
		);
	});
}

export function registerSiftSearchTool(pi) {
	pi.registerTool({
		name: "sift_search",
		label: "Sift Search",
		description:
			"Run a Sift hybrid/vector/BM25 search over the local codebase. " +
			"Exposes the full Sift CLI surface including agentic multi-turn search, " +
			"strategy selection, and planner configuration. " +
			"Use this when you need Sift-specific features (agent mode, planner strategies, " +
			"or explicit retriever control). Use `codebase_search` for simpler conceptual queries.",
		promptSnippet:
			"Search the codebase with Sift using hybrid BM25+vector retrieval",
		promptGuidelines: [
			"Use `sift_search` when you need agentic multi-turn search (`agent: true`), " +
				"a specific Sift strategy (`page-index-hybrid`, `path-hybrid`, `bm25`, `vector`), " +
				"or explicit retriever/planner configuration.",
			"Use `codebase_search` for simple conceptual queries where the default strategy is sufficient.",
			"Prefer `page-index-hybrid` for broad conceptual discovery (strongest recall + structural reranking).",
			"Prefer `path-hybrid` when the query is filename, path, module, or symbol-heavy.",
			"Prefer `bm25` for fast lexical-only queries.",
			"Enable `agent: true` with `agentMode: 'graph'` for deep multi-turn research across disconnected code regions.",
			"Enable `agent: true` with `plannerStrategy: 'model-driven'` for LLM-guided planning.",
		],
		parameters: Type.Object({
			query: Type.String({
				description:
					"Search query. Be specific and include key terms, function names, or concepts.",
			}),
			path: Type.Optional(
				Type.String({
					description:
						"Directory or file path to search within. Default: repository root ('.'); absolute paths inside the repo are normalized to repo-relative paths so .siftignore applies.",
					default: ".",
				}),
			),
			strategy: Type.Optional(
				Type.String({
					description:
						"Retrieval strategy. " +
						"page-index-hybrid (default): best all-around — BM25 + phrase + path-fuzzy + segment-fuzzy + vector with structural reranking. " +
						"path-hybrid: best for filename/path/module/symbol queries. " +
						"bm25: fast lexical-only. " +
						"vector: semantic-only. " +
						"page-index-llm/jina/gemma: heavier semantic passes.",
					default: DEFAULT_STRATEGY,
				}),
			),
			agent: Type.Optional(
				Type.Boolean({
					description:
						"Enable agentic multi-turn search. Sift will plan and execute multiple " +
						"retrieval turns autonomously to answer complex questions.",
					default: false,
				}),
			),
			agentMode: Type.Optional(
				Type.String({
					description:
						"Agent planning mode when agent is enabled. " +
						"linear: bounded linear planning (default). " +
						"graph: bounded graph exploration for disconnected code regions.",
					enum: ["linear", "graph"],
					default: "linear",
				}),
			),
			plannerStrategy: Type.Optional(
				Type.String({
					description:
						"Planner strategy when agent mode is enabled. " +
						"heuristic: rule-based planning (default, faster). " +
						"model-driven: LLM-guided planning (slower, more thorough).",
					enum: ["heuristic", "model-driven"],
					default: "heuristic",
				}),
			),
			limit: Type.Optional(
				Type.Number({
					description: "Maximum number of hits to return. Default: 10.",
					default: DEFAULT_LIMIT,
				}),
			),
			retrieverTimeoutMs: Type.Optional(
				Type.Number({
					description:
						"Timeout (ms) for individual retriever operations. " +
						"Increase for large repos or cold caches. Default: 30000.",
					default: DEFAULT_RETRIEVER_TIMEOUT_MS,
				}),
			),
			timeoutMs: Type.Optional(
				Type.Number({
					description:
						"Overall command timeout (ms). Increase for agent mode or large repos. " +
						"Default: 60000.",
					default: DEFAULT_TIMEOUT_MS,
				}),
			),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
			const binaryPath = resolveSiftBinary();
			if (!binaryPath) {
				return {
					content: [
						{
							type: "text",
							text: "Error: sift binary not found. Install rupurt/sift on PATH or set SIFT_PATH.",
						},
					],
					details: { operation: "sift_search", error: "binary_not_found" },
					isError: true,
				};
			}

			const projectRoot = process.cwd();
			const args = buildSiftArgs(params, projectRoot);
			const scope = args.at(-2) ?? ".";
			const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;

			const startedAt = Date.now();
			try {
				const { stdout, stderr } = await runSift(
					binaryPath,
					args,
					timeoutMs,
					projectRoot,
				);
				const elapsedMs = Date.now() - startedAt;
				const result = parseSiftOutput(stdout, stderr);
				const runtimeDirs = ensureSiftRuntimeDirs(projectRoot);
				await recordRetrievalEvidence(projectRoot, {
					backend: "sift",
					sourceKind: "code",
					query: params.query,
					strategy: params.strategy ?? DEFAULT_STRATEGY,
					scope,
					status: "ok",
					hitCount: result.hits.length,
					elapsedMs,
					cachePath: runtimeDirs.searchCache,
					result: {
						hits: result.hits.map((hit) => ({
							path: hit.path,
							score: hit.score,
							lineStart: hit.lineStart,
							lineEnd: hit.lineEnd,
						})),
						agent: params.agent ?? false,
						agentMode: params.agentMode ?? null,
						plannerStrategy: params.plannerStrategy ?? null,
					},
				});

				try {
					const { debugLog } = await import("../debug-logger.js");
					debugLog("sift_search", {
						query: params.query,
						strategy: params.strategy ?? DEFAULT_STRATEGY,
						agent: params.agent ?? false,
						path: scope,
						hitCount: result.hits.length,
						elapsedMs,
						binary: binaryPath,
						searchCache: runtimeDirs.searchCache,
					});
				} catch {
					// Telemetry must not change search semantics.
				}

				const lines = [
					`Sift search: "${params.query}"`,
					`Strategy: ${params.strategy ?? DEFAULT_STRATEGY}${params.agent ? ` | agent: ${params.agentMode ?? "linear"} | planner: ${params.plannerStrategy ?? "heuristic"}` : ""}`,
					`Scope: ${scope}`,
					`Search cache: ${runtimeDirs.searchCache}`,
					`Hits: ${result.hits.length} | Elapsed: ${elapsedMs}ms`,
					"",
				];

				for (const hit of result.hits.slice(0, params.limit ?? DEFAULT_LIMIT)) {
					lines.push(`[${hit.score.toFixed(3)}] ${hit.path}`);
					if (hit.content) {
						const snippet = hit.content.replace(/\s+/g, " ").trim();
						if (snippet.length > 0) {
							lines.push(
								`  ${snippet.slice(0, 300)}${snippet.length > 300 ? "..." : ""}`,
							);
						}
					}
					lines.push("");
				}

				if (result.stderr && result.stderr.trim().length > 0) {
					lines.push(`--- stderr ---\n${result.stderr.trim()}`);
				}

				return {
					content: [{ type: "text", text: lines.join("\n") }],
					details: {
						operation: "sift_search",
						query: params.query,
						strategy: params.strategy ?? DEFAULT_STRATEGY,
						agent: params.agent ?? false,
						elapsedMs,
						hitCount: result.hits.length,
						hits: result.hits,
						searchCache: runtimeDirs.searchCache,
					},
				};
			} catch (err) {
				const elapsedMs = Date.now() - startedAt;
				const message = getErrorMessage(err);
				await recordRetrievalEvidence(projectRoot, {
					backend: "sift",
					sourceKind: "code",
					query: params.query,
					strategy: params.strategy ?? DEFAULT_STRATEGY,
					scope,
					status: "error",
					hitCount: 0,
					elapsedMs,
					error: message,
				});
				return {
					content: [
						{
							type: "text",
							text: `Sift search failed after ${elapsedMs}ms: ${message}`,
						},
					],
					details: {
						operation: "sift_search",
						query: params.query,
						strategy: params.strategy ?? DEFAULT_STRATEGY,
						elapsedMs,
						error: message,
					},
					isError: true,
				};
			}
		},
	});
}
