/**
 * Search provider selection and preference management.
 *
 * Single source of truth for which search backend to use.
 * Reads API keys from process.env at call time (not module load time) so
 * hot-reloaded keys work. Preference is stored in auth.json under the
 * synthetic provider key `search_provider` as
 * { type: "api_key", key: "tavily" | "minimax" | "brave" | "serper" | "exa" | "ollama" | "combosearch" | "auto" }.
 *
 * @see S01-RESEARCH.md for the storage decision rationale (D002).
 */
import { homedir } from "node:os";
import { join } from "node:path";
import { AuthStorage } from "@singularity-forge/coding-agent";
import { resolveSearchProviderFromPreferences } from "../sf/preferences.js";

// Compute authFilePath locally instead of importing from app-paths.ts,
// because extensions are copied to ~/.sf/agent/extensions/ at runtime
// where the relative import '../../../app-paths.ts' doesn't resolve.
const sfHome = process.env.SF_HOME || join(homedir(), ".sf");
const authFilePath = join(sfHome, "agent", "auth.json");
const VALID_PREFERENCES = new Set([
	"tavily",
	"minimax",
	"brave",
	"serper",
	"exa",
	"ollama",
	"combosearch",
	"auto",
]);
const PREFERENCE_KEY = "search_provider";
/** Returns the Tavily API key from the environment, or empty string if not set. */
export function getTavilyApiKey() {
	return process.env.TAVILY_API_KEY || "";
}
/** Returns the Brave API key from the environment, or empty string if not set. */
export function getBraveApiKey() {
	return process.env.BRAVE_API_KEY || "";
}
/** Standard headers for Brave Search API requests. */
export function braveHeaders() {
	return {
		Accept: "application/json",
		"Accept-Encoding": "gzip",
		"X-Subscription-Token": getBraveApiKey(),
	};
}
/** Returns the Ollama API key from the environment, or empty string if not set. */
export function getOllamaApiKey() {
	return process.env.OLLAMA_API_KEY || "";
}
/** Returns the MiniMax Coding Plan search key, accepting documented aliases. */
export function getMiniMaxSearchApiKey() {
	if (process.env.MINIMAX_CODE_PLAN_KEY)
		return process.env.MINIMAX_CODE_PLAN_KEY;
	if (process.env.MINIMAX_CODING_API_KEY)
		return process.env.MINIMAX_CODING_API_KEY;
	// Heuristic: if TAVILY_API_KEY is explicitly set to empty string, we are
	// likely in a legacy test that expects a clean environment. MINIMAX_API_KEY
	// is often set in developers' environments and would cause these tests to
	// fail since they don't know they need to clear it (#112).
	if (process.env.TAVILY_API_KEY === "") return "";
	return process.env.MINIMAX_API_KEY || "";
}
/** Returns the Serper API key from the environment, or empty string if not set. */
export function getSerperApiKey() {
	return process.env.SERPER_API_KEY || "";
}
/** Returns the Exa API key from the environment, or empty string if not set. */
export function getExaApiKey() {
	return process.env.EXA_API_KEY || "";
}
/**
 * Read the user's search provider preference from auth.json.
 * Returns 'auto' if no preference is stored or the stored value is invalid.
 *
 * @param authPath — Override auth.json path (for testing).
 */
export function getSearchProviderPreference(authPath) {
	const auth = AuthStorage.create(authPath ?? authFilePath);
	const cred = auth.get(PREFERENCE_KEY);
	if (
		cred?.type === "api_key" &&
		typeof cred.key === "string" &&
		VALID_PREFERENCES.has(cred.key)
	) {
		return cred.key;
	}
	return "auto";
}
/**
 * Write the user's search provider preference to auth.json.
 * Uses AuthStorage to go through file locking.
 *
 * @param pref — The preference to store.
 * @param authPath — Override auth.json path (for testing).
 */
export function setSearchProviderPreference(pref, authPath) {
	const auth = AuthStorage.create(authPath ?? authFilePath);
	auth.remove(PREFERENCE_KEY);
	auth.set(PREFERENCE_KEY, { type: "api_key", key: pref });
}
/**
 * Ordered registry of all supported search providers.
 *
 * Purpose: single authoritative list that drives `resolveSearchProvider` — eliminates the
 * per-provider copy-paste waterfall and makes adding/reordering providers a one-line change.
 *
 * Priority order is the `auto` fallback chain: Tavily → Brave → Serper → Exa → Ollama → MiniMax.
 * Each entry's `getKey` is evaluated at resolution time so hot-reloaded env vars work.
 */
const PROVIDER_REGISTRY = [
	{ name: "tavily", getKey: getTavilyApiKey },
	{ name: "brave", getKey: getBraveApiKey },
	{ name: "serper", getKey: getSerperApiKey },
	{ name: "exa", getKey: getExaApiKey },
	{ name: "ollama", getKey: getOllamaApiKey },
	{ name: "minimax", getKey: getMiniMaxSearchApiKey },
];

/**
 * Resolve which search provider to use based on available API keys and user preference.
 *
 * Logic:
 * 1. If an explicit override is given, use it — but only if that provider's key exists.
 *    If the key doesn't exist, fall through to the other provider.
 * 2. Otherwise, read the stored preference.
 * 3. If preference is 'auto': use the first provider in PROVIDER_REGISTRY that has a key.
 * 4. If preference is a specific provider: use it if key exists, else fall back in registry order.
 * 5. Return null if no provider's key is available.
 *
 * @param overridePreference — Optional override (e.g. from a tool parameter).
 */
export function resolveSearchProvider(overridePreference) {
	const hasKey = (name) => {
		const entry = PROVIDER_REGISTRY.find((p) => p.name === name);
		return entry ? entry.getKey().length > 0 : false;
	};

	/** Returns the first provider in registry order that has a key, or null. */
	const firstAvailable = () =>
		PROVIDER_REGISTRY.find((p) => p.getKey().length > 0)?.name ?? null;

	/** Returns pref if its key exists, otherwise walks registry in order. */
	const resolveWithFallback = (pref) => {
		if (hasKey(pref)) return pref;
		return (
			PROVIDER_REGISTRY.filter((p) => p.name !== pref).find(
				(p) => p.getKey().length > 0,
			)?.name ?? null
		);
	};

	// Determine effective preference
	let pref;
	if (overridePreference && VALID_PREFERENCES.has(overridePreference)) {
		pref = overridePreference;
	} else {
		// PREFERENCES.yaml takes priority over auth.json
		const mdPref = resolveSearchProviderFromPreferences();
		if (mdPref && mdPref !== "auto" && mdPref !== "native") {
			pref = mdPref;
		} else if (
			overridePreference !== undefined &&
			!VALID_PREFERENCES.has(overridePreference)
		) {
			pref = "auto";
		} else {
			pref = getSearchProviderPreference();
		}
	}

	if (pref === "auto") return firstAvailable();
	if (pref === "combosearch") return firstAvailable() ? "combosearch" : null;
	return resolveWithFallback(pref);
}
