/**
 * 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 { mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { Type } from "@sinclair/typebox";
import {
	buildSiftEnv,
	chooseSiftRetrievers,
	ensureSiftRuntimeDirs,
	resolveSiftBinary,
	resolveSiftLogging,
	resolveSiftSearchScope,
	resolveSiftSearchScopes,
} from "../code-intelligence.js";
import { getErrorMessage } from "../error-utils.js";
import { recordRetrievalEvidence } from "../retrieval-evidence.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 = 300_000;
const DEFAULT_TIMEOUT_MS = 600_000;

/**
 * Build the sift CLI argument list from tool parameters.
 */
function buildSiftArgs(
	params,
	projectRoot = process.cwd(),
	scopeOverride = null,
) {
	const scope =
		scopeOverride ?? 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),
	];

	// Scope-aware retriever selection: if the caller didn't explicitly pass
	// retrievers/reranking, derive safe defaults from the query scope.
	// Explicit overrides always win; for repo-root scope the helper returns
	// bm25+phrase (no vector) to avoid the full-workspace embedding hang
	// (#vector-hang-fix). For scoped subdirs, vector + reranking are enabled.
	const scopedDefaults = chooseSiftRetrievers(scope, projectRoot);
	const effectiveRetrievers = params.retrievers ?? scopedDefaults.retrievers;
	const effectiveReranking = params.reranking ?? scopedDefaults.reranking;
	args.push("--retrievers", String(effectiveRetrievers));
	args.push("--reranking", String(effectiveReranking));

	// --verbose gives more progress info from the Rust binary when the
	// operator explicitly asked for debug-level sift logging.
	const siftLogLevel = (process.env.SF_SIFT_LOG_LEVEL ?? "info").toLowerCase();
	if (siftLogLevel === "debug" || siftLogLevel === "trace") {
		args.push("--verbose");
	}

	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,
	};
}
function mergeSiftResults(results, limit) {
	const seen = new Set();
	const hits = [];
	for (const result of results) {
		for (const hit of result.hits) {
			const key = `${hit.path}:${hit.lineStart ?? ""}:${hit.lineEnd ?? ""}:${hit.content}`;
			if (seen.has(key)) continue;
			seen.add(key);
			hits.push(hit);
		}
	}
	hits.sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0));
	return {
		hits: hits.slice(0, limit),
		stderr: results
			.map((result) => result.stderr)
			.filter((text) => text && text.trim().length > 0)
			.join("\n"),
	};
}

/**
 * Execute a sift search with the given parameters.
 */
/**
 * Count vector-index sector files in the global sift cache to estimate
 * indexing progress. Returns { sectorCount, cacheSizeMb }.
 */
function estimateVectorIndexProgress(_projectRoot) {
	try {
		const globalCache = join(
			process.env.HOME ?? "/tmp",
			".cache",
			"sift",
			"search",
			"artifacts",
			"indexes",
		);
		if (!statSync(globalCache, { throwIfNoEntry: false })?.isDirectory()) {
			return { sectorCount: 0, cacheSizeMb: 0 };
		}
		let totalSectors = 0;
		let totalBytes = 0;
		for (const entry of readdirSync(globalCache)) {
			const sectorDir = join(globalCache, entry, "sectors");
			try {
				const files = readdirSync(sectorDir);
				totalSectors += files.length;
				for (const f of files) {
					const s = statSync(join(sectorDir, f), { throwIfNoEntry: false });
					if (s) totalBytes += s.size;
				}
			} catch {
				// skip unreadable dirs
			}
		}
		return {
			sectorCount: totalSectors,
			cacheSizeMb: Math.round(totalBytes / (1024 * 1024)),
		};
	} catch {
		return { sectorCount: 0, cacheSizeMb: 0 };
	}
}

function runSift(
	binaryPath,
	args,
	timeoutMs,
	projectRoot,
	logEnv = {},
	logPath = null,
) {
	return new Promise((resolve, reject) => {
		ensureSiftRuntimeDirs(projectRoot);
		if (logPath) {
			mkdirSync(dirname(logPath), { recursive: true });
			writeFileSync(logPath, "", "utf-8");
		}

		// If vector retrievers are in play, start a progress poller that
		// writes index-build state into the log file every 30 seconds so
		// an operator can tail it during long cold-cache runs.
		const usesVector = args.includes("vector");
		let progressTimer = null;
		if (usesVector && logPath) {
			const baseline = estimateVectorIndexProgress(projectRoot);
			let lastSectors = baseline.sectorCount;
			progressTimer = setInterval(() => {
				const { sectorCount, cacheSizeMb } =
					estimateVectorIndexProgress(projectRoot);
				const delta = sectorCount - lastSectors;
				lastSectors = sectorCount;
				const line = `[${new Date().toISOString()}] vector-index progress: ${sectorCount} sectors (${cacheSizeMb} MB total)${delta > 0 ? ` (+${delta} since last check)` : ""}\n`;
				try {
					writeFileSync(logPath, line, { encoding: "utf-8", flag: "a" });
				} catch {
					// ignore
				}
			}, 30_000);
		}

		const _child = execFile(
			binaryPath,
			args,
			{
				cwd: projectRoot,
				encoding: "utf-8",
				env: { ...buildSiftEnv(projectRoot, process.env), ...logEnv },
				maxBuffer: 16 * 1024 * 1024,
				timeout: timeoutMs,
			},
			(error, stdout, stderr) => {
				if (progressTimer) clearInterval(progressTimer);
				if (logPath && stderr) {
					try {
						writeFileSync(logPath, stderr, { encoding: "utf-8", flag: "a" });
					} catch {
						// log write failure must not affect search result
					}
				}
				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/all-repo expands to bounded top-level shards instead of passing '.' directly to Sift.",
					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 scopePlan = resolveSiftSearchScopes(projectRoot, params.path);
			const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
			const { env: logEnv, logPath } = resolveSiftLogging(projectRoot);

			const startedAt = Date.now();
			try {
				const perScopeTimeoutMs = scopePlan.sharded
					? Math.max(5_000, Math.floor(timeoutMs / scopePlan.scopes.length))
					: timeoutMs;
				const scopedResults = [];
				for (const scope of scopePlan.scopes) {
					const args = buildSiftArgs(params, projectRoot, scope);
					const { stdout, stderr } = await runSift(
						binaryPath,
						args,
						perScopeTimeoutMs,
						projectRoot,
						logEnv,
						logPath,
					);
					const parsed = parseSiftOutput(stdout, stderr);
					scopedResults.push({ ...parsed, scope });
				}
				const elapsedMs = Date.now() - startedAt;
				const result = mergeSiftResults(
					scopedResults,
					params.limit ?? DEFAULT_LIMIT,
				);
				const runtimeDirs = ensureSiftRuntimeDirs(projectRoot);
				await recordRetrievalEvidence(projectRoot, {
					backend: "sift",
					sourceKind: "code",
					query: params.query,
					strategy: params.strategy ?? DEFAULT_STRATEGY,
					scope: scopePlan.sharded
						? `shards:${scopePlan.scopes.join(",")}`
						: scopePlan.scopes[0],
					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: scopePlan.sharded ? scopePlan.scopes : scopePlan.scopes[0],
						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: ${scopePlan.sharded ? `all repo shards (${scopePlan.scopes.join(", ")})` : scopePlan.scopes[0]}`,
					`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.hits.length === 0 && logPath) {
					lines.push(`(stage diagnostic: ${logPath})`);
				}

				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,
						sharded: scopePlan.sharded,
						scopes: scopePlan.scopes,
						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: scopePlan.sharded
						? `shards:${scopePlan.scopes.join(",")}`
						: scopePlan.scopes[0],
					status: "error",
					hitCount: 0,
					elapsedMs,
					error: message,
				});
				const logHint = logPath ? `\n(stage diagnostic: ${logPath})` : "";
				return {
					content: [
						{
							type: "text",
							text: `Sift search failed after ${elapsedMs}ms: ${message}${logHint}`,
						},
					],
					details: {
						operation: "sift_search",
						query: params.query,
						strategy: params.strategy ?? DEFAULT_STRATEGY,
						sharded: scopePlan.sharded,
						scopes: scopePlan.scopes,
						elapsedMs,
						error: message,
					},
					isError: true,
				};
			}
		},
	});
}
