/**
 * Optional code-intelligence backends for SF.
 *
 * CODEBASE.md stays the durable baseline. Codebase indexers are optional
 * accelerators for local code retrieval.
 */

import { spawn, spawnSync } from "node:child_process";
import {
	existsSync,
	mkdirSync,
	readFileSync,
	statSync,
	writeFileSync,
} from "node:fs";
import { delimiter, join, resolve } from "node:path";

import type {
	CodebaseIndexerBackendName,
	CodebaseMapPreferences,
} from "./preferences-types.js";

export const PROJECT_RAG_MCP_SERVER_NAME = "project-rag";
const PROJECT_RAG_BINARY_NAME =
	process.platform === "win32" ? "project-rag.exe" : "project-rag";
const SIFT_BINARY_NAME = process.platform === "win32" ? "sift.exe" : "sift";

const PROJECT_RAG_SOURCE_CANDIDATES = [
	"vendor/project-rag",
	"vendor/brainwires/project-rag",
	"third_party/project-rag",
	"third_party/brainwires/project-rag",
	"tools/project-rag",
	"project-rag",
] as const;

export interface ProjectRagMcpConfig {
	command?: string;
	args?: string[];
	cwd?: string;
	env?: Record<string, string>;
	url?: string;
}

interface McpConfigFile {
	mcpServers?: Record<string, ProjectRagMcpConfig>;
	servers?: Record<string, ProjectRagMcpConfig>;
	[key: string]: unknown;
}

export type CodebaseIndexerStatus = "disabled" | "configured" | "missing";

export interface CodebaseIndexerDetection {
	backend: CodebaseIndexerBackendName;
	status: CodebaseIndexerStatus;
	reason: string;
	serverName?: string;
	configPath?: string;
	command?: string;
	binaryPath?: string;
	sourceDir?: string;
}

export interface CodebaseIndexerBackend<
	TDetection extends CodebaseIndexerDetection = CodebaseIndexerDetection,
> {
	name: CodebaseIndexerBackendName;
	label: string;
	detect(
		projectRoot: string,
		prefs?: CodebaseMapPreferences,
		env?: NodeJS.ProcessEnv,
	): TDetection;
	formatStatus(
		projectRoot: string,
		prefs?: CodebaseMapPreferences,
		env?: NodeJS.ProcessEnv,
	): string;
	buildContextLines(
		projectRoot: string,
		prefs?: CodebaseMapPreferences,
		env?: NodeJS.ProcessEnv,
	): string[];
}

export interface ProjectRagDetection extends CodebaseIndexerDetection {
	backend: "projectRag";
	status: "disabled" | "configured" | "missing";
	serverName?: string;
	configPath?: string;
	command?: string;
	binaryPath?: string;
	sourceDir?: string;
	reason: string;
}

export interface SiftDetection extends CodebaseIndexerDetection {
	backend: "sift";
	status: "disabled" | "configured" | "missing";
	command?: string;
	binaryPath?: string;
	reason: string;
}

export interface EnsureProjectRagMcpConfigResult {
	configPath: string;
	serverName: string;
	status: "created" | "updated" | "unchanged";
}

export interface ProjectRagBuildResult {
	sourceDir: string;
	binaryPath: string;
	buildJobs: string;
	stdout: string;
	stderr: string;
}

export interface SiftIndexWarmupResult {
	status: "started" | "skipped" | "unavailable" | "error";
	reason: string;
	command?: string;
	args?: string[];
	markerPath?: string;
}

interface SiftIndexWarmupOptions {
	env?: NodeJS.ProcessEnv;
	force?: boolean;
	ttlMs?: number;
	query?: string;
	limit?: number;
	retrieverTimeoutMs?: number;
	hardTimeoutSec?: number;
	spawnFn?: typeof spawn;
	now?: number;
}

const DEFAULT_SIFT_WARMUP_TTL_MS = 6 * 60 * 60 * 1000;
const DEFAULT_SIFT_WARMUP_QUERY =
	"repo architecture source tests entrypoints configuration";
const DEFAULT_SIFT_WARMUP_LIMIT = 1;
const DEFAULT_SIFT_WARMUP_RETRIEVER_TIMEOUT_MS = 30_000;
const DEFAULT_SIFT_WARMUP_HARD_TIMEOUT_SEC = 1800;
const SIFT_WARMUP_KILL_GRACE_SEC = 10;

function readJsonConfig(configPath: string): McpConfigFile {
	if (!existsSync(configPath)) return {};
	const raw = readFileSync(configPath, "utf-8");
	const parsed = JSON.parse(raw) as McpConfigFile;
	return parsed && typeof parsed === "object" ? parsed : {};
}

function readMcpConfigEntries(
	projectRoot: string,
): Array<{ name: string; config: ProjectRagMcpConfig; configPath: string }> {
	const entries: Array<{
		name: string;
		config: ProjectRagMcpConfig;
		configPath: string;
	}> = [];
	const seen = new Set<string>();
	for (const configPath of [
		join(projectRoot, ".mcp.json"),
		join(projectRoot, ".sf", "mcp.json"),
	]) {
		try {
			const data = readJsonConfig(configPath);
			const servers = data.mcpServers ?? data.servers;
			if (!servers || typeof servers !== "object") continue;
			for (const [name, config] of Object.entries(servers)) {
				if (seen.has(name)) continue;
				seen.add(name);
				entries.push({ name, config, configPath });
			}
		} catch {
			// Malformed optional MCP config should not block SF startup.
		}
	}
	return entries;
}

function configLooksLikeProjectRag(
	name: string,
	config: ProjectRagMcpConfig,
): boolean {
	const haystack = [
		name,
		config.command ?? "",
		...(config.args ?? []),
		config.cwd ?? "",
	]
		.join(" ")
		.toLowerCase();
	return /project[-_]?rag|brainwires/.test(haystack);
}

function normalizeProjectRoot(projectRoot: string): string {
	return resolve(projectRoot);
}

function commandExists(
	command: string | undefined,
	env: NodeJS.ProcessEnv = process.env,
): boolean {
	if (!command) return false;
	return lookupExecutable(command, env) !== null;
}

export function detectProjectRag(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): ProjectRagDetection {
	const mode = prefs?.project_rag ?? "auto";
	if (mode === "off") {
		return {
			backend: "projectRag",
			status: "disabled",
			reason: "codebase.project_rag is off",
		};
	}

	const configuredServer = prefs?.project_rag_server?.trim();
	const normalizedRoot = normalizeProjectRoot(projectRoot);
	const binaryPath =
		resolveProjectRagBinaryForProject(normalizedRoot, env) ?? undefined;
	const sourceDir = findProjectRagSourceDir(normalizedRoot, env) ?? undefined;
	const entries = readMcpConfigEntries(normalizedRoot);
	const match = entries.find(({ name, config }) =>
		configuredServer
			? name === configuredServer
			: configLooksLikeProjectRag(name, config),
	);

	if (match) {
		const configuredCommandExists = commandExists(match.config.command, env);
		return {
			backend: "projectRag",
			status: "configured",
			serverName: match.name,
			configPath: match.configPath,
			command: match.config.command,
			binaryPath,
			sourceDir,
			reason: configuredCommandExists
				? "project-rag MCP server configured"
				: "project-rag MCP server configured but command is not currently executable",
		};
	}

	return {
		backend: "projectRag",
		status: "missing",
		binaryPath,
		sourceDir,
		reason:
			mode === "required"
				? "codebase.project_rag is required but no project-rag MCP server is configured"
				: "no project-rag MCP server configured",
	};
}

function lookupExecutable(
	command: string,
	env: NodeJS.ProcessEnv = process.env,
): string | null {
	if (/[\\/]/.test(command) && existsSync(command)) return command;
	const pathValue = env.PATH ?? "";
	for (const dir of pathValue.split(delimiter).filter(Boolean)) {
		const candidate = join(dir, command);
		if (existsSync(candidate)) return candidate;
	}
	return null;
}

interface ResolvedHardTimeout {
	binary: string;
	wrapperArgs: string[];
	timeoutSec: number;
}

function resolveSiftWarmupHardTimeoutSec(
	env: NodeJS.ProcessEnv,
	override?: number,
): number | null {
	if (env.SF_SIFT_HARD_TIMEOUT_DISABLE === "1") return null;
	if (override !== undefined) {
		return Number.isFinite(override) && override > 0
			? Math.floor(override)
			: null;
	}
	const raw = env.SF_SIFT_HARD_TIMEOUT_SEC?.trim();
	if (raw) {
		const parsed = Number.parseInt(raw, 10);
		if (parsed === 0) return null;
		if (Number.isFinite(parsed) && parsed > 0) return parsed;
	}
	return DEFAULT_SIFT_WARMUP_HARD_TIMEOUT_SEC;
}

function resolveSiftWarmupTimeoutWrapper(
	env: NodeJS.ProcessEnv,
	timeoutSec: number,
): ResolvedHardTimeout | null {
	if (process.platform === "win32") return null;
	const candidates =
		process.platform === "darwin"
			? ["gtimeout", "timeout"]
			: ["timeout", "gtimeout"];
	for (const candidate of candidates) {
		const binary = lookupExecutable(candidate, env);
		if (binary) {
			return {
				binary,
				wrapperArgs: [
					`--kill-after=${SIFT_WARMUP_KILL_GRACE_SEC}`,
					String(timeoutSec),
				],
				timeoutSec,
			};
		}
	}
	return null;
}

export function resolveProjectRagBinary(
	env: NodeJS.ProcessEnv = process.env,
): string | null {
	const explicit =
		env.SF_PROJECT_RAG_BIN?.trim() || env.PROJECT_RAG_BIN?.trim();
	if (explicit) return explicit;
	return lookupExecutable("project-rag", env);
}

export function resolveSiftBinary(
	env: NodeJS.ProcessEnv = process.env,
): string | null {
	const explicit = env.SIFT_PATH?.trim();
	if (explicit) return explicit;
	return (
		lookupExecutable(SIFT_BINARY_NAME, env) ??
		(SIFT_BINARY_NAME === "sift" ? null : lookupExecutable("sift", env))
	);
}

export function detectSift(
	_projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): SiftDetection {
	if (prefs?.indexer_backend === "none") {
		return {
			backend: "sift",
			status: "disabled",
			reason: "codebase.indexer_backend is none",
		};
	}

	const explicit = env.SIFT_PATH?.trim();
	const binaryPath = resolveSiftBinary(env) ?? undefined;
	if (!binaryPath) {
		return {
			backend: "sift",
			status: "missing",
			reason:
				"sift binary not found on PATH; set SIFT_PATH or install rupurt/sift.",
		};
	}

	if (explicit && !commandExists(explicit, env)) {
		return {
			backend: "sift",
			status: "missing",
			command: explicit,
			binaryPath: explicit,
			reason: "SIFT_PATH is set but does not resolve to an executable file.",
		};
	}

	return {
		backend: "sift",
		status: "configured",
		command: binaryPath,
		binaryPath,
		reason: explicit
			? "sift binary resolved from SIFT_PATH"
			: "sift binary found on PATH",
	};
}

function isFreshMarker(
	markerPath: string,
	now: number,
	ttlMs: number,
): boolean {
	try {
		const stat = statSync(markerPath);
		if (now - stat.mtimeMs >= ttlMs) return false;
		const parsed = JSON.parse(readFileSync(markerPath, "utf-8")) as {
			schemaVersion?: number;
			cwd?: string;
			args?: unknown;
		};
		return (
			parsed.schemaVersion === 2 &&
			Array.isArray(parsed.args) &&
			parsed.args.at(-2) === "."
		);
	} catch {
		return false;
	}
}

export function ensureSiftIndexWarmup(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	options: SiftIndexWarmupOptions = {},
): SiftIndexWarmupResult {
	const env = options.env ?? process.env;
	const backendName = resolveEffectiveCodebaseIndexerBackendName(
		projectRoot,
		prefs,
		env,
	);
	if (backendName !== "sift") {
		return {
			status: "skipped",
			reason: `effective codebase indexer is ${backendName}`,
		};
	}

	const detection = detectSift(projectRoot, prefs, env);
	if (detection.status !== "configured" || !detection.binaryPath) {
		return {
			status: "unavailable",
			reason: detection.reason,
		};
	}

	const markerPath = join(
		projectRoot,
		".sf",
		"runtime",
		"sift-index-warmup.json",
	);
	const now = options.now ?? Date.now();
	const ttlMs = options.ttlMs ?? DEFAULT_SIFT_WARMUP_TTL_MS;
	if (!options.force && isFreshMarker(markerPath, now, ttlMs)) {
		return {
			status: "skipped",
			reason: "recent sift warmup marker exists",
			markerPath,
		};
	}

	const siftArgs = [
		"search",
		"--json",
		"--strategy",
		"page-index-hybrid",
		"--limit",
		String(options.limit ?? DEFAULT_SIFT_WARMUP_LIMIT),
		"--retriever-timeout-ms",
		String(
			options.retrieverTimeoutMs ??
				DEFAULT_SIFT_WARMUP_RETRIEVER_TIMEOUT_MS,
		),
		".",
		options.query ?? DEFAULT_SIFT_WARMUP_QUERY,
	];

	const hardTimeoutSec = resolveSiftWarmupHardTimeoutSec(
		env,
		options.hardTimeoutSec,
	);
	const wrapper =
		hardTimeoutSec !== null
			? resolveSiftWarmupTimeoutWrapper(env, hardTimeoutSec)
			: null;
	const command = wrapper ? wrapper.binary : detection.binaryPath;
	const args = wrapper
		? [...wrapper.wrapperArgs, detection.binaryPath, ...siftArgs]
		: siftArgs;
	const startedReason = wrapper
		? `sift page-index-hybrid warmup started (hard cap ${wrapper.timeoutSec}s via ${wrapper.binary})`
		: hardTimeoutSec === null
			? "sift page-index-hybrid warmup started (hard cap disabled)"
			: "sift page-index-hybrid warmup started (no timeout(1)/gtimeout on PATH; running unbounded)";

	try {
		mkdirSync(join(projectRoot, ".sf", "runtime"), { recursive: true });
		writeFileSync(
			markerPath,
			`${JSON.stringify(
				{
					schemaVersion: 2,
					startedAt: new Date(now).toISOString(),
					command,
					cwd: projectRoot,
					args,
					siftBinary: detection.binaryPath,
					hardTimeoutSec: wrapper?.timeoutSec ?? null,
				},
				null,
				2,
			)}\n`,
			"utf-8",
		);

		const child = (options.spawnFn ?? spawn)(command, args, {
			cwd: projectRoot,
			env,
			stdio: "ignore",
			detached: true,
		});
		child.unref();
		return {
			status: "started",
			reason: startedReason,
			command,
			args,
			markerPath,
		};
	} catch (err) {
		return {
			status: "error",
			reason: err instanceof Error ? err.message : String(err),
			command,
			args,
			markerPath,
		};
	}
}

function projectRagBinaryFromSource(sourceDir: string): string | null {
	const candidate = join(
		sourceDir,
		"target",
		"release",
		PROJECT_RAG_BINARY_NAME,
	);
	return existsSync(candidate) ? candidate : null;
}

export function resolveProjectRagBuildJobs(
	env: NodeJS.ProcessEnv = process.env,
): string {
	const configured =
		env.SF_PROJECT_RAG_BUILD_JOBS?.trim() || env.CARGO_BUILD_JOBS?.trim();
	if (!configured) return "2";

	const parsed = Number.parseInt(configured, 10);
	return Number.isFinite(parsed) && parsed > 0 ? String(parsed) : "2";
}

export function findProjectRagSourceDir(
	projectRoot: string,
	env: NodeJS.ProcessEnv = process.env,
): string | null {
	const explicit =
		env.SF_PROJECT_RAG_SOURCE?.trim() || env.PROJECT_RAG_SOURCE?.trim();
	const candidates = [
		...(explicit ? [explicit] : []),
		...PROJECT_RAG_SOURCE_CANDIDATES.map((relativePath) =>
			join(normalizeProjectRoot(projectRoot), relativePath),
		),
	];
	for (const candidate of candidates) {
		const manifestPath = join(candidate, "Cargo.toml");
		if (!existsSync(manifestPath)) continue;
		try {
			const manifest = readFileSync(manifestPath, "utf-8");
			if (
				/name\s*=\s*"project-rag"/.test(manifest) ||
				/project-rag/i.test(candidate)
			) {
				return resolve(candidate);
			}
		} catch {
			// Optional vendored source discovery should never block SF startup.
		}
	}
	return null;
}

export function resolveProjectRagBinaryForProject(
	projectRoot: string,
	env: NodeJS.ProcessEnv = process.env,
): string | null {
	const explicitOrPath = resolveProjectRagBinary(env);
	if (explicitOrPath) return explicitOrPath;

	const sourceDir = findProjectRagSourceDir(projectRoot, env);
	if (sourceDir) {
		const builtBinary = projectRagBinaryFromSource(sourceDir);
		if (builtBinary) return builtBinary;
	}

	for (const relativePath of [
		join("target", "release", PROJECT_RAG_BINARY_NAME),
		join(".bin", PROJECT_RAG_BINARY_NAME),
		join("bin", PROJECT_RAG_BINARY_NAME),
	]) {
		const candidate = join(normalizeProjectRoot(projectRoot), relativePath);
		if (existsSync(candidate)) return candidate;
	}

	return null;
}

export function buildProjectRagMcpServerConfig(
	projectRoot: string = process.cwd(),
	env: NodeJS.ProcessEnv = process.env,
): ProjectRagMcpConfig {
	const command = resolveProjectRagBinaryForProject(projectRoot, env);
	if (!command) {
		const sourceDir = findProjectRagSourceDir(projectRoot, env);
		throw new Error(
			sourceDir
				? `project-rag source found at ${sourceDir}, but no release binary exists. Run /sf codebase rag build first.`
				: "project-rag binary not found. Set SF_PROJECT_RAG_BIN, install project-rag on PATH, or vendor Brainwires/project-rag under vendor/project-rag.",
		);
	}
	return {
		command,
		env: {
			RUST_LOG: env.RUST_LOG ?? "info",
		},
	};
}

export function buildProjectRagBinary(
	projectRoot: string,
	env: NodeJS.ProcessEnv = process.env,
): ProjectRagBuildResult {
	const sourceDir = findProjectRagSourceDir(projectRoot, env);
	if (!sourceDir) {
		throw new Error(
			"project-rag source not found. Vendor Brainwires/project-rag under vendor/project-rag or set SF_PROJECT_RAG_SOURCE.",
		);
	}
	const cargo = lookupExecutable("cargo", env);
	if (!cargo) {
		throw new Error(
			"cargo not found in PATH; cannot build vendored project-rag.",
		);
	}

	const buildJobs = resolveProjectRagBuildJobs(env);
	const result = spawnSync(cargo, ["build", "--release"], {
		cwd: sourceDir,
		env: { ...process.env, ...env, CARGO_BUILD_JOBS: buildJobs },
		encoding: "utf-8",
		maxBuffer: 20 * 1024 * 1024,
	});
	const stdout = result.stdout ?? "";
	const stderr = result.stderr ?? "";
	if (result.error) {
		throw new Error(`cargo build failed to start: ${result.error.message}`);
	}
	if (result.status !== 0) {
		throw new Error(
			`cargo build --release failed with exit ${result.status ?? "unknown"}:\n${stderr || stdout}`.trim(),
		);
	}

	const binaryPath = projectRagBinaryFromSource(sourceDir);
	if (!binaryPath) {
		throw new Error(
			`cargo build completed, but ${join(sourceDir, "target", "release", PROJECT_RAG_BINARY_NAME)} was not found.`,
		);
	}

	return { sourceDir, binaryPath, buildJobs, stdout, stderr };
}

export function ensureProjectRagMcpConfig(
	projectRoot: string,
	env: NodeJS.ProcessEnv = process.env,
): EnsureProjectRagMcpConfigResult {
	const resolvedProjectRoot = normalizeProjectRoot(projectRoot);
	const configPath = join(resolvedProjectRoot, ".mcp.json");
	const alreadyPresent = existsSync(configPath);
	const existing = readJsonConfig(configPath);
	const desiredServer = buildProjectRagMcpServerConfig(
		resolvedProjectRoot,
		env,
	);
	const previousServers = existing.mcpServers ?? {};
	const current = previousServers[PROJECT_RAG_MCP_SERVER_NAME];
	const unchanged =
		JSON.stringify(current ?? null) === JSON.stringify(desiredServer) &&
		existing.mcpServers !== undefined;

	if (unchanged) {
		return {
			configPath,
			serverName: PROJECT_RAG_MCP_SERVER_NAME,
			status: "unchanged",
		};
	}

	const nextConfig: McpConfigFile = {
		...existing,
		mcpServers: {
			...previousServers,
			[PROJECT_RAG_MCP_SERVER_NAME]: desiredServer,
		},
	};
	writeFileSync(
		configPath,
		`${JSON.stringify(nextConfig, null, 2)}\n`,
		"utf-8",
	);
	return {
		configPath,
		serverName: PROJECT_RAG_MCP_SERVER_NAME,
		status: alreadyPresent ? "updated" : "created",
	};
}

function formatToolPrefix(serverName: string): string {
	return `mcp__${serverName.replace(/[^A-Za-z0-9_]/g, "_")}__`;
}

function buildProjectRagContextLines(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): string[] {
	const detection = detectProjectRag(projectRoot, prefs, env);
	const lines: string[] = [];

	if (detection.status === "disabled") {
		lines.push("- Project RAG: disabled by `codebase.project_rag: off`.");
	} else if (detection.status === "configured" && detection.serverName) {
		const prefix = formatToolPrefix(detection.serverName);
		lines.push(
			`- Project RAG: configured as MCP server \`${detection.serverName}\`.`,
		);
		lines.push(
			"- Use Project RAG for broad code retrieval before manual file-by-file reading, " +
				"especially conceptual queries, exact identifiers, schema fields, and git-history questions.",
		);
		lines.push(
			`- Expected MCP tool prefix: \`${prefix}\` ` +
				`(for example \`${prefix}index_codebase\`, \`${prefix}query_codebase\`, ` +
				`\`${prefix}search_by_filters\`, \`${prefix}find_definition\`, ` +
				`\`${prefix}find_references\`, \`${prefix}get_call_graph\`).`,
		);
		lines.push(
			prefs?.project_rag_auto_index === false
				? "- Do not auto-index unless explicitly needed; query existing indexes first. " +
						"If any Project RAG tool is missing or fails, continue with `.sf/CODEBASE.md`, native `grep`/`find`/`ls`, `lsp`, `codebase_search`, and scout."
				: "- Index first if the backend is stale or empty; use incremental indexing when available. " +
						"If any Project RAG tool is missing or fails, continue with `.sf/CODEBASE.md`, native `grep`/`find`/`ls`, `lsp`, `codebase_search`, and scout.",
		);
	} else {
		lines.push(
			"- Project RAG: not configured. This is optional; continue with `.sf/CODEBASE.md`, native `grep`/`find`/`ls`, `lsp`, `codebase_search`, and scout.",
		);
		lines.push(
			"- To enable later: build/install Brainwires/project-rag, then run `/sf codebase rag init` or set `SF_PROJECT_RAG_BIN` before initializing MCP config.",
		);
	}

	return lines;
}

function buildSiftContextLines(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): string[] {
	const detection = detectSift(projectRoot, prefs, env);
	const lines: string[] = [];

	if (detection.status === "disabled") {
		lines.push(
			"- Codebase indexer: disabled by `codebase.indexer_backend: none`.",
		);
	} else if (detection.status === "configured" && detection.binaryPath) {
		lines.push(`- Sift: configured as local CLI \`${detection.binaryPath}\`.`);
		lines.push(
			"- Use Sift for broad code retrieval before manual file-by-file reading, " +
				"especially conceptual queries, exact identifiers, approximate file/path intent, and synthesis-ready snippets.",
		);
		lines.push(
			'- Command shape: `sift search --json <path> "<query>"`. ' +
				"Use `--strategy page-index-hybrid` for the strongest direct-search preset, " +
				'`--strategy path-hybrid` for path-heavy queries, and `sift search <path> --agent "<task>"` for bounded planner-driven exploration.',
		);
		lines.push(
			"- Sift uses a sector-aware cache in the platform cache directory, typically `~/.cache/sift`; " +
				"if the CLI is missing or fails, continue with `.sf/CODEBASE.md`, native `grep`/`find`/`ls`, `lsp`, and scout.",
		);
	} else {
		lines.push(
			"- Sift: not available. This is optional; continue with `.sf/CODEBASE.md`, native `grep`/`find`/`ls`, `lsp`, and scout.",
		);
		lines.push(
			"- To enable later: install `rupurt/sift` on PATH or set `SIFT_PATH` to the sift binary.",
		);
	}

	return lines;
}

function buildNoCodebaseIndexerContextLines(): string[] {
	return [
		"- Codebase indexer: disabled by `codebase.indexer_backend: none`; continue with `.sf/CODEBASE.md`, native `grep`/`find`/`ls`, `lsp`, and scout.",
	];
}

export function resolveCodebaseIndexerBackendName(
	prefs?: CodebaseMapPreferences,
): CodebaseIndexerBackendName {
	return prefs?.indexer_backend ?? "projectRag";
}

export function resolveEffectiveCodebaseIndexerBackendName(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): CodebaseIndexerBackendName {
	if (prefs?.indexer_backend) return prefs.indexer_backend;
	const sift = detectSift(projectRoot, prefs, env);
	if (sift.status === "configured") return "sift";
	return "projectRag";
}

export function getCodebaseIndexerBackend(
	prefsOrName?: CodebaseMapPreferences | CodebaseIndexerBackendName,
): CodebaseIndexerBackend {
	const name =
		typeof prefsOrName === "string"
			? prefsOrName
			: resolveCodebaseIndexerBackendName(prefsOrName);
	return CODEBASE_INDEXER_BACKENDS[name];
}

export function detectCodebaseIndexer(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): CodebaseIndexerDetection {
	const backendName = resolveEffectiveCodebaseIndexerBackendName(
		projectRoot,
		prefs,
		env,
	);
	return getCodebaseIndexerBackend(backendName).detect(projectRoot, prefs, env);
}

export function formatCodebaseIndexerStatus(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): string {
	const backendName = resolveEffectiveCodebaseIndexerBackendName(
		projectRoot,
		prefs,
		env,
	);
	return getCodebaseIndexerBackend(backendName).formatStatus(
		projectRoot,
		prefs,
		env,
	);
}

export function buildCodeIntelligenceContextBlock(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): string {
	const backendName = resolveEffectiveCodebaseIndexerBackendName(
		projectRoot,
		prefs,
		env,
	);
	const lines = [
		"[PROJECT CODE INTELLIGENCE]",
		"",
		"- Durable baseline: use `.sf/CODEBASE.md` for structural orientation and persistent project knowledge.",
		...getCodebaseIndexerBackend(backendName).buildContextLines(
			projectRoot,
			prefs,
			env,
		),
	];

	return `\n\n${lines.join("\n")}`;
}

export function formatProjectRagStatus(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): string {
	const detection = detectProjectRag(projectRoot, prefs, env);
	const lines = ["Project RAG Status", ""];
	lines.push(`Status: ${detection.status}`);
	lines.push(`Reason: ${detection.reason}`);
	if (detection.serverName) lines.push(`Server: ${detection.serverName}`);
	if (detection.configPath) lines.push(`Config: ${detection.configPath}`);
	if (detection.command) lines.push(`Command: ${detection.command}`);
	if (detection.binaryPath) lines.push(`Binary: ${detection.binaryPath}`);
	if (detection.sourceDir) lines.push(`Source: ${detection.sourceDir}`);
	if (detection.status === "configured" && detection.command) {
		lines.push(
			`Operational: ${commandExists(detection.command, env) ? "yes" : "no - configured command is missing"}`,
		);
	} else if (detection.binaryPath) {
		lines.push(
			"Operational: no - binary exists but MCP config is missing; run /sf codebase rag init.",
		);
	} else if (detection.sourceDir) {
		lines.push(
			"Operational: no - source exists but release binary is missing; run /sf codebase rag build.",
		);
	} else {
		lines.push("Operational: no - binary/source not found.");
	}
	lines.push("");
	lines.push(
		"Project RAG is optional. SF falls back to CODEBASE.md, native grep/find/ls, lsp, codebase_search, and scout when it is unavailable.",
	);
	lines.push(
		"When configured, agents should use index_codebase, query_codebase, search_by_filters, find_definition, find_references, and get_call_graph before manual file-by-file reading.",
	);
	return lines.join("\n");
}

export function formatSiftStatus(
	projectRoot: string,
	prefs?: CodebaseMapPreferences,
	env: NodeJS.ProcessEnv = process.env,
): string {
	const detection = detectSift(projectRoot, prefs, env);
	const lines = ["Sift Status", ""];
	lines.push(`Status: ${detection.status}`);
	lines.push(`Reason: ${detection.reason}`);
	if (detection.command) lines.push(`Command: ${detection.command}`);
	if (detection.binaryPath) lines.push(`Binary: ${detection.binaryPath}`);
	if (detection.status === "configured" && detection.command) {
		lines.push(
			`Operational: ${commandExists(detection.command, env) ? "yes" : "no - configured command is missing"}`,
		);
	} else {
		lines.push(
			"Operational: no - install rupurt/sift on PATH or set SIFT_PATH.",
		);
	}
	lines.push("");
	lines.push(
		"Sift is optional. SF falls back to CODEBASE.md, native grep/find/ls, lsp, and scout when it is unavailable.",
	);
	lines.push(
		'When configured, agents should use `sift search --json <path> "<query>"`; `page-index-hybrid` is the strongest direct-search preset and `path-hybrid` is best for path-heavy queries.',
	);
	lines.push(
		"Sift reuses a sector-aware cache in the platform cache directory, typically ~/.cache/sift.",
	);
	return lines.join("\n");
}

function formatNoCodebaseIndexerStatus(): string {
	return [
		"Codebase Indexer Status",
		"",
		"Status: disabled",
		"Reason: codebase.indexer_backend is none",
		"Operational: no - optional codebase indexer disabled.",
		"",
		"SF will use CODEBASE.md, native grep/find/ls, lsp, and scout for codebase orientation.",
	].join("\n");
}

export const PROJECT_RAG_CODEBASE_INDEXER_BACKEND: CodebaseIndexerBackend<ProjectRagDetection> =
	{
		name: "projectRag",
		label: "Project RAG",
		detect: detectProjectRag,
		formatStatus: formatProjectRagStatus,
		buildContextLines: buildProjectRagContextLines,
	};

export const SIFT_CODEBASE_INDEXER_BACKEND: CodebaseIndexerBackend<SiftDetection> =
	{
		name: "sift",
		label: "Sift",
		detect: detectSift,
		formatStatus: formatSiftStatus,
		buildContextLines: buildSiftContextLines,
	};

export const NO_CODEBASE_INDEXER_BACKEND: CodebaseIndexerBackend = {
	name: "none",
	label: "None",
	detect: () => ({
		backend: "none",
		status: "disabled",
		reason: "codebase.indexer_backend is none",
	}),
	formatStatus: formatNoCodebaseIndexerStatus,
	buildContextLines: buildNoCodebaseIndexerContextLines,
};

export const CODEBASE_INDEXER_BACKENDS: Record<
	CodebaseIndexerBackendName,
	CodebaseIndexerBackend
> = {
	projectRag: PROJECT_RAG_CODEBASE_INDEXER_BACKEND,
	sift: SIFT_CODEBASE_INDEXER_BACKEND,
	none: NO_CODEBASE_INDEXER_BACKEND,
};
