/**
 * SF Doctor — Provider & Integration Health Checks
 *
 * Fast, deterministic checks for external service configuration.
 * Checks key presence in auth.json and environment variables — no HTTP calls,
 * no network I/O, always sub-10ms.
 *
 * Covers:
 *   - LLM providers required by the effective model preferences (per phase)
 *   - Remote questions channel if configured (Slack/Discord/Telegram token)
 *   - Optional search/tool integrations (Brave, Tavily, Jina, Context7)
 */
import { existsSync } from "node:fs";
import { AuthStorage } from "@singularity-forge/coding-agent";
import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import {
	getConfiguredEnvApiKey,
	getGatedEnvVarKey,
} from "./provider-env-auth.js";
import { couldBeVaultUri } from "./vault-credential-resolver.js";

// ── Model → Provider ID mapping ───────────────────────────────────────────────
/**
 * Infer the auth provider ID from a model string.
 * Handles plain model IDs ("claude-sonnet-4-6") and prefixed ones ("openrouter/deepseek").
 */
function modelToProviderId(model) {
	if (!model) return null;
	// Explicit provider prefix (e.g. "openrouter/deepseek-r1")
	if (model.includes("/")) {
		const prefix = model.split("/")[0].toLowerCase();
		// Map known prefixes to registry IDs
		const prefixMap = {
			"anthropic-vertex": "anthropic-vertex",
			openrouter: "openrouter",
			groq: "groq",
			mistral: "mistral",
			google: "google",
			"google-vertex": "google-vertex",
			anthropic: "anthropic",
			openai: "openai",
			zai: "zai",
			minimax: "minimax",
			"kimi-coding": "kimi-coding",
			xiaomi: "xiaomi",
			ollama: "ollama",
			"ollama-cloud": "ollama-cloud",
			opencode: "opencode",
			"opencode-go": "opencode-go",
			"alibaba-coding-plan": "alibaba-coding-plan",
		};
		if (prefixMap[prefix]) return prefixMap[prefix];
	}
	const lower = model.toLowerCase();
	if (lower.startsWith("claude")) return "anthropic";
	if (
		lower.startsWith("gpt-") ||
		lower.startsWith("o1") ||
		lower.startsWith("o3")
	)
		return "openai";
	if (lower.startsWith("gemini")) return "google";
	if (lower.startsWith("llama") || lower.startsWith("mixtral")) return "groq";
	if (lower.startsWith("mistral") || lower.startsWith("codestral"))
		return "mistral";
	return null;
}
/** Collect all model strings from effective preferences across all phases. */
function collectConfiguredModelProviders() {
	const providers = new Set();
	try {
		const loaded = loadEffectiveSFPreferences();
		const models = loaded?.preferences?.models;
		if (!models) {
			// Default: Anthropic
			providers.add("anthropic");
			return providers;
		}
		const modelEntries =
			typeof models === "object" ? Object.values(models) : [];
		for (const entry of modelEntries) {
			if (typeof entry === "string") {
				const pid = modelToProviderId(entry);
				if (pid) providers.add(pid);
				continue;
			}
			if (typeof entry === "object" && entry !== null && "model" in entry) {
				const configuredProvider =
					"provider" in entry ? entry.provider : undefined;
				if (
					typeof configuredProvider === "string" &&
					configuredProvider.trim().length > 0
				) {
					providers.add(configuredProvider);
					continue;
				}
				const modelId = String(entry.model);
				const pid = modelToProviderId(modelId);
				if (pid) providers.add(pid);
			}
		}
	} catch {
		// Preferences not readable — assume Anthropic as default
		providers.add("anthropic");
	}
	if (providers.size === 0) providers.add("anthropic");
	return providers;
}
function resolveKey(providerId) {
	const info = PROVIDER_REGISTRY.find((p) => p.id === providerId);
	if (
		providerId === "anthropic-vertex" &&
		process.env.ANTHROPIC_VERTEX_PROJECT_ID
	) {
		return { found: true, source: "env", backedOff: false };
	}
	// Check auth.json
	const authPath = getAuthPath();
	if (existsSync(authPath)) {
		try {
			const auth = AuthStorage.create(authPath);
			const creds = auth.getCredentialsForProvider(providerId);
			if (creds.length > 0) {
				// Filter out empty placeholder keys (from skipped onboarding)
				const hasRealKey = creds.some(
					(c) => c.type === "oauth" || (c.type === "api_key" && c.key),
				);
				if (hasRealKey) {
					return {
						found: true,
						source: "auth.json",
						backedOff: auth.areAllCredentialsBackedOff(providerId),
					};
				}
			}
		} catch {
			// auth.json malformed — fall through to env check
		}
	}
	// Check environment variable using the authoritative env var resolution
	// (handles multi-var lookups like ANTHROPIC_OAUTH_TOKEN || ANTHROPIC_API_KEY,
	//  COPILOT_GITHUB_TOKEN || GH_TOKEN || GITHUB_TOKEN, Vertex ADC, Bedrock, etc.)
	if (getConfiguredEnvApiKey(providerId)) {
		return { found: true, source: "env", backedOff: false };
	}
	// Check for vault:// URIs in env vars (late-binding resolution)
	// Supports vault://secret/path#field syntax via HashiCorp Vault
	if (info?.envVar && couldBeVaultUri(process.env[info.envVar])) {
		return { found: true, source: "vault", backedOff: false };
	}
	// Fallback for providers not covered by getEnvApiKey (search/tool providers like
	// Brave, Tavily, Jina, Context7) — still gated by providerEnvAuth setting.
	if (info?.envVar && getGatedEnvVarKey(providerId, info.envVar)) {
		return { found: true, source: "env", backedOff: false };
	}
	return { found: false, source: "none", backedOff: false };
}
// ── Individual check groups ────────────────────────────────────────────────────
/**
 * Providers that can serve models normally associated with another provider.
 * Key = the provider whose models can be served, Value = alternative providers to check.
 * Copilot is disabled from SF default routing.
 */
const PROVIDER_ROUTES = {
	anthropic: [],
	openai: ["openai-codex"],
	google: ["google-gemini-cli"],
};
/**
 * Providers that use external CLI authentication (not API keys).
 * These are always considered "ok" — the host CLI handles auth.
 */
const CLI_AUTH_PROVIDERS = new Set([
	"claude-code",
	"openai-codex",
	"google-gemini-cli",
]);
function checkLlmProviders() {
	const required = collectConfiguredModelProviders();
	const results = [];
	const lookups = new Map();
	for (const providerId of required) {
		if (CLI_AUTH_PROVIDERS.has(providerId)) {
			lookups.set(providerId, {
				found: true,
				source: "cli",
				backedOff: false,
			});
			continue;
		}
		lookups.set(providerId, resolveKey(providerId));
	}
	const hasUsableConfiguredProvider = [...lookups.values()].some(
		(lookup) => lookup.found && !lookup.backedOff,
	);
	for (const providerId of required) {
		// CLI-authenticated providers don't need API keys — skip key check
		if (CLI_AUTH_PROVIDERS.has(providerId)) {
			const info = PROVIDER_REGISTRY.find((p) => p.id === providerId);
			results.push({
				name: providerId,
				label: info?.label ?? providerId,
				category: "llm",
				status: "ok",
				message: `${info?.label ?? providerId} — CLI auth (no key needed)`,
				required: true,
			});
			continue;
		}
		const info = PROVIDER_REGISTRY.find((p) => p.id === providerId);
		const label =
			providerId === "anthropic-vertex"
				? "Anthropic Vertex"
				: (info?.label ?? providerId);
		const lookup = lookups.get(providerId) ?? resolveKey(providerId);
		if (!lookup.found) {
			// Check if a cross-provider can serve this provider's models
			const routes = PROVIDER_ROUTES[providerId];
			const routeProvider = routes?.find(
				(routeId) => resolveKey(routeId).found,
			);
			if (routeProvider) {
				const routeInfo = PROVIDER_REGISTRY.find((p) => p.id === routeProvider);
				const routeLabel = routeInfo?.label ?? routeProvider;
				results.push({
					name: providerId,
					label,
					category: "llm",
					status: "ok",
					message: `${label} — available via ${routeLabel}`,
					required: true,
				});
				continue;
			}
			if (hasUsableConfiguredProvider) {
				results.push({
					name: providerId,
					label,
					category: "llm",
					status: "unconfigured",
					message: `${label} — not configured (another configured model route is available)`,
					required: false,
				});
				continue;
			}
			const envVar =
				providerId === "anthropic-vertex"
					? "ANTHROPIC_VERTEX_PROJECT_ID"
					: (info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`);
			results.push({
				name: providerId,
				label,
				category: "llm",
				status: "error",
				message: `${label} — not configured`,
				detail:
					providerId === "anthropic-vertex"
						? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
						: info?.hasOAuth
							? `Run /keys to authenticate`
							: `Set ${envVar} or run /keys`,
				required: true,
			});
		} else if (lookup.backedOff) {
			results.push({
				name: providerId,
				label,
				category: "llm",
				status: "warning",
				message: `${label} — all credentials backed off (rate limited)`,
				detail: `SF will retry automatically`,
				required: true,
			});
		} else {
			results.push({
				name: providerId,
				label,
				category: "llm",
				status: "ok",
				source: lookup.source,
				message: `${label} — key present (${lookup.source})`,
				required: true,
			});
		}
	}
	return results;
}
function checkRemoteQuestionsProvider() {
	try {
		const loaded = loadEffectiveSFPreferences();
		const rq = loaded?.preferences?.remote_questions;
		if (!rq) return null;
		const channel = rq.channel;
		if (!channel) return null;
		const providerMap = {
			slack: "slack_bot",
			discord: "discord_bot",
			telegram: "telegram_bot",
		};
		const providerId = providerMap[channel.toLowerCase()];
		if (!providerId) return null;
		const info = PROVIDER_REGISTRY.find((p) => p.id === providerId);
		const label = info?.label ?? channel;
		const lookup = resolveKey(providerId);
		if (!lookup.found) {
			const autoResolvable =
				rq.auto_resolve_on_timeout === true && rq.auto_resolve_strategy;
			return {
				name: providerId,
				label,
				category: "remote",
				status: autoResolvable ? "unconfigured" : "warning",
				message: autoResolvable
					? `${label} — token not found (remote questions auto-resolve on timeout)`
					: `${label} — channel configured but token not found`,
				detail: info?.envVar
					? `Set ${info.envVar} or run /keys`
					: `Run /keys to configure`,
				required: !autoResolvable,
			};
		}
		return {
			name: providerId,
			label,
			category: "remote",
			status: "ok",
			message: `${label} — token present (${lookup.source})`,
			required: true,
		};
	} catch {
		return null;
	}
}
function checkOptionalProviders() {
	const optional = ["brave", "tavily", "jina", "context7"];
	const results = [];
	// Determine which search providers are configured so we can suppress
	// "not configured" noise for alternative search providers when at least
	// one is already active (e.g. don't warn about missing BRAVE_API_KEY
	// when Tavily is configured).
	const searchProviderIds = PROVIDER_REGISTRY.filter(
		(p) => p.category === "search",
	).map((p) => p.id);
	const hasAnySearchProvider = searchProviderIds.some(
		(id) => resolveKey(id).found,
	);
	for (const providerId of optional) {
		const info = PROVIDER_REGISTRY.find((p) => p.id === providerId);
		if (!info) continue;
		const lookup = resolveKey(providerId);
		// Skip unconfigured search providers when another search provider is active
		if (!lookup.found && hasAnySearchProvider && info.category === "search") {
			continue;
		}
		results.push({
			name: providerId,
			label: info.label,
			category: info.category,
			status: lookup.found ? "ok" : "unconfigured",
			message: lookup.found
				? `${info.label} — key present (${lookup.source})`
				: `${info.label} — not configured (optional)`,
			detail:
				!lookup.found && info.envVar
					? `Set ${info.envVar} to enable`
					: undefined,
			required: false,
		});
	}
	return results;
}
// ── Public API ─────────────────────────────────────────────────────────────────
/**
 * Run all provider checks: required LLM keys, remote questions channel, optional tools.
 * Fast (sub-10ms) — reads auth.json and env vars only, no network I/O.
 */
export function runProviderChecks() {
	const results = [];
	results.push(...checkLlmProviders());
	const remoteCheck = checkRemoteQuestionsProvider();
	if (remoteCheck) results.push(remoteCheck);
	results.push(...checkOptionalProviders());
	return results;
}
/**
 * Format provider check results as a human-readable report string.
 */
export function formatProviderReport(results) {
	if (results.length === 0) return "No provider checks run.";
	const lines = [];
	const groups = {};
	for (const r of results) {
		(groups[r.category] ??= []).push(r);
	}
	const categoryLabels = {
		llm: "LLM Providers",
		remote: "Notifications",
		search: "Search",
		tool: "Tools",
	};
	for (const [cat, items] of Object.entries(groups)) {
		lines.push(`${categoryLabels[cat] ?? cat}:`);
		for (const item of items) {
			const icon =
				item.status === "ok"
					? "✓"
					: item.status === "warning"
						? "⚠"
						: item.status === "error"
							? "✗"
							: "·";
			lines.push(`  ${icon} ${item.message}`);
			if (item.detail && item.status !== "ok") {
				lines.push(`    ${item.detail}`);
			}
		}
	}
	return lines.join("\n");
}
/**
 * Summarise check results to a compact widget-friendly string.
 * Returns null if all required providers are ok.
 */
export function summariseProviderIssues(results) {
	const errors = results.filter((r) => r.required && r.status === "error");
	const warnings = results.filter((r) => r.required && r.status === "warning");
	if (errors.length === 0 && warnings.length === 0) return null;
	const parts = [];
	if (errors.length > 0) parts.push(`✗ ${errors[0].label} key missing`);
	if (warnings.length > 0 && errors.length === 0)
		parts.push(`⚠ ${warnings[0].label} backed off`);
	if (errors.length + warnings.length > 1)
		parts.push(`(+${errors.length + warnings.length - 1} more)`);
	return parts.join(" ");
}
