/**
 * SF Vault Secret Resolver
 *
 * Purpose: Resolve secrets from HashiCorp Vault using URI syntax (vault://path#field).
 * Provides a secure alternative to storing plaintext API keys in config files.
 *
 * Consumer: pi-ai provider config layer for LLM model credentials.
 *
 * URI Format: vault://secret/path/to/secret#fieldname
 * Example: vault://secret/anthropic/prod#api_key
 *
 * Auth Chain:
 * 1. VAULT_ADDR and VAULT_TOKEN environment variables
 * 2. ~/.vault-token file (if VAULT_TOKEN not set)
 * 3. AppRole (Kubernetes-style) from VAULT_ROLE_ID and VAULT_SECRET_ID env vars
 * 4. Fails open: if vault unavailable, returns null (caller can fall back to plaintext)
 *
 * Caching: Results cached in memory to avoid repeated vault requests.
 * No persistent cache (vault secrets can change).
 */
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { getErrorMessage } from "./error-utils.js";
import { logWarning } from "./workflow-logger.js";

/**
 * In-memory cache for resolved vault secrets.
 * Key: full URI string, Value: { secret, timestamp, ttlMs }
 */
const secretCache = new Map();
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Parse a vault:// URI into components.
 *
 * Returns { path, field, vaultAddr, token } or { error: string }
 */
export function parseVaultUri(uri) {
	try {
		if (!uri || typeof uri !== "string") {
			return { error: "URI must be a non-empty string" };
		}

		if (!uri.startsWith("vault://")) {
			return { error: 'URI must start with "vault://"' };
		}

		const afterScheme = uri.substring(8); // Remove "vault://"
		const [pathPart, fieldPart] = afterScheme.split("#");

		if (!pathPart || !fieldPart) {
			return {
				error:
					"Invalid vault URI format. Expected: vault://path/to/secret#fieldname",
			};
		}

		const path = pathPart.trim();
		const field = fieldPart.trim();

		if (!path || !field) {
			return { error: "Path and field must be non-empty" };
		}

		const vaultAddr = process.env.VAULT_ADDR || "http://127.0.0.1:8200";
		const token = resolveVaultToken();

		return { path, field, vaultAddr, token };
	} catch (err) {
		return {
			error: `Failed to parse vault URI: ${getErrorMessage(err)}`,
		};
	}
}

/**
 * Resolve the vault token from environment or file.
 *
 * Tries in order:
 * 1. VAULT_TOKEN env var
 * 2. ~/.vault-token file
 * 3. AppRole (VAULT_ROLE_ID + VAULT_SECRET_ID env vars) — not yet implemented
 * 4. null (caller should fail open)
 */
export function resolveVaultToken() {
	// 1. Environment variable
	if (process.env.VAULT_TOKEN) {
		return process.env.VAULT_TOKEN;
	}

	// 2. Home directory token file
	try {
		const tokenPath = `${homedir()}/.vault-token`;
		if (existsSync(tokenPath)) {
			const token = readFileSync(tokenPath, "utf8").trim();
			if (token) return token;
		}
	} catch {
		// Silently fail — will try AppRole next
	}

	// 3. AppRole (future: implement after Vault client is added)
	// For now, return null to signal "try fallback"
	return null;
}

/**
 * Fetch a secret from Vault using the KV v2 API.
 *
 * Returns the secret object or null if unavailable.
 */
async function fetchVaultSecret(path, vaultAddr, token) {
	if (!token) {
		return null; // No auth token available
	}

	try {
		const url = `${vaultAddr}/v1/secret/data/${path}`;
		const response = await fetch(url, {
			method: "GET",
			headers: {
				"X-Vault-Token": token,
			},
		});

		if (!response.ok) {
			if (response.status === 404) {
				return null; // Secret not found
			}
			if (response.status === 403) {
				return null; // Unauthorized
			}
			throw new Error(
				`Vault request failed: ${response.status} ${response.statusText}`,
			);
		}

		const data = await response.json();
		return data.data?.data ?? null; // KV v2 nests data twice
	} catch (err) {
		// Log error but don't throw — fail open
		logWarning(
			"vault-resolver",
			`Vault fetch failed for ${path}: ${getErrorMessage(err)}`,
		);
		return null;
	}
}

/**
 * Resolve a vault:// URI to its actual secret value.
 *
 * Returns { resolved: true, value: secretValue } on success,
 * or { resolved: false, reason: string } if vault unavailable.
 *
 * Caches results in memory for TTL (default 5 minutes).
 */
export async function resolveVaultSecret(
	uri,
	cacheTtlMs = DEFAULT_CACHE_TTL_MS,
) {
	// Check memory cache first
	const cached = secretCache.get(uri);
	if (cached && Date.now() - cached.timestamp < cacheTtlMs) {
		return { resolved: true, value: cached.secret };
	}

	// Parse URI
	const parsed = parseVaultUri(uri);
	if (parsed.error) {
		return { resolved: false, reason: `Invalid URI: ${parsed.error}` };
	}

	const { path, field, vaultAddr, token } = parsed;

	// Fetch from vault
	const secret = await fetchVaultSecret(path, vaultAddr, token);
	if (!secret) {
		return { resolved: false, reason: "Vault secret not found or unavailable" };
	}

	// Extract field
	const value = secret[field];
	if (value === undefined) {
		return {
			resolved: false,
			reason: `Field "${field}" not found in vault secret`,
		};
	}

	// Cache result
	secretCache.set(uri, { secret: value, timestamp: Date.now() });

	return { resolved: true, value };
}

/**
 * Check if a string is a vault:// URI (without trying to parse it).
 */
export function isVaultUri(value) {
	return typeof value === "string" && value.startsWith("vault://");
}

/**
 * Clear the secret cache (useful for testing or when vault secrets change).
 */
export function clearVaultCache() {
	secretCache.clear();
}

/**
 * Get cache statistics (for monitoring/debugging).
 */
export function getVaultCacheStats() {
	return {
		size: secretCache.size,
		keys: Array.from(secretCache.keys()),
	};
}

/**
 * Resolve a value that might be a vault:// URI or plaintext.
 *
 * If the value starts with "vault://", attempts to resolve it from Vault.
 * If resolution fails or value is plaintext, returns the original value.
 *
 * Purpose: Graceful fallback for config values that may or may not use vault.
 */
export async function resolveSecret(value, opts = {}) {
	if (!isVaultUri(value)) {
		// Plaintext value — return as-is
		return { resolved: true, value, source: "plaintext" };
	}

	// Try vault
	const result = await resolveVaultSecret(value, opts.cacheTtlMs);
	if (result.resolved) {
		return { resolved: true, value: result.value, source: "vault" };
	}

	// Fallback to plaintext (fail open)
	if (opts.failOpen !== false) {
		return {
			resolved: true,
			value,
			source: "plaintext",
			warning: `Vault resolution failed: ${result.reason}. Falling back to plaintext value.`,
		};
	}

	// Strict mode — throw if vault unavailable
	throw new Error(
		`Vault resolution failed: ${result.reason}. Set failOpen: true to use plaintext fallback.`,
	);
}
