/**
 * Singularity Memory (SM) Client
 *
 * Purpose: Async interface to Singularity Memory platform for cross-tool knowledge federation.
 * Provides graceful fallback to local-only memory if SM unavailable.
 *
 * Consumer: memory-store.js sync agent, memory-tools.js cross-project recall.
 *
 * Integration model (layered):
 * - SF maintains local SQLite as hot cache (working memory)
 * - SM holds durable cross-tool store (episodic memory)
 * - Async fire-and-forget sync from SF → SM
 * - Cross-project recall optional (when SM available)
 */

/**
 * Initialize SM client and test connectivity.
 * Returns { connected: boolean, version: string | null, error?: string }
 *
 * Tries to connect to SM at SINGULARITY_MEMORY_ADDR (default: http://localhost:8080).
 * SM is opt-in: set SM_ENABLED=true to activate. Disabled by default.
 */
import { getErrorMessage } from "./error-utils.js";

/**
 * Resolve the explicit tenant/project boundary for Singularity Memory calls.
 *
 * Purpose: prevent `SM_ENABLED=true` from becoming an implicit all-tenant
 * cross-project recall switch.
 *
 * Consumer: SM sync/query operations before sending data to the memory server.
 */
export function resolveSmScope(opts = {}) {
	const tenantId =
		typeof opts.tenantId === "string" && opts.tenantId.trim()
			? opts.tenantId.trim()
			: typeof opts.tenant === "string" && opts.tenant.trim()
				? opts.tenant.trim()
				: (
						process.env.SM_TENANT_ID ||
						process.env.SINGULARITY_MEMORY_TENANT_ID ||
						""
					).trim();
	const projectId =
		typeof opts.projectId === "string" && opts.projectId.trim()
			? opts.projectId.trim()
			: (
					process.env.SM_PROJECT_ID ||
					process.env.SINGULARITY_MEMORY_PROJECT_ID ||
					""
				).trim();
	return {
		tenantId: tenantId || null,
		projectId: projectId || null,
	};
}

export async function initializeSmClient() {
	// SM is opt-in — disabled unless explicitly enabled
	if (process.env.SM_ENABLED !== "true") {
		return {
			connected: false,
			version: null,
			reason: "disabled (set SM_ENABLED=true to enable)",
		};
	}

	const addr = process.env.SINGULARITY_MEMORY_ADDR || "http://localhost:8080";

	try {
		const response = await fetch(`${addr}/v1/health`, {
			method: "GET",
			signal: AbortSignal.timeout(3000), // 3-second timeout
		});

		if (!response.ok) {
			return {
				connected: false,
				version: null,
				reason: `SM health check returned ${response.status}`,
			};
		}

		const health = await response.json();
		return {
			connected: true,
			version: health.version || "unknown",
			addr,
		};
	} catch (err) {
		return {
			connected: false,
			version: null,
			reason: `SM unreachable at ${addr}: ${getErrorMessage(err)}`,
		};
	}
}

/**
 * Sync a memory to Singularity Memory.
 * Fire-and-forget: returns immediately; actual sync happens in background.
 *
 * Memory shape:
 * {
 *   id: string (uuid)
 *   category: "gotcha" | "convention" | "architecture" | "pattern" | "environment" | "preference"
 *   content: string
 *   confidence: number [0, 1]
 *   source_unit_type?: "milestone" | "slice" | "task"
 *   source_unit_id?: string
 *   created_at: number (ms)
 *   updated_at: number (ms)
 *   hit_count: number (confidence booster)
 * }
 */
export async function syncMemoryToSm(memory, opts = {}) {
	if (!opts.smConnected) {
		// SM not available — skip sync
		return { queued: false, reason: "SM not connected" };
	}

	const addr =
		opts.smAddr ||
		process.env.SINGULARITY_MEMORY_ADDR ||
		"http://localhost:8080";
	const scope = resolveSmScope(opts);
	if (!scope.tenantId) {
		return {
			queued: false,
			reason:
				"SM tenant scope missing (set SM_TENANT_ID or SINGULARITY_MEMORY_TENANT_ID)",
		};
	}

	// Queue sync in background (fire-and-forget)
	setImmediate(async () => {
		try {
			const response = await fetch(`${addr}/v1/memories`, {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					memory,
					source: "sf", // Identify source as SF instance
					tenantId: scope.tenantId,
					...(scope.projectId ? { projectId: scope.projectId } : {}),
				}),
				signal: AbortSignal.timeout(5000),
			});

			if (!response.ok) {
				console.warn(
					`[sm-client] Failed to sync memory ${memory.id}: ${response.status}`,
				);
			}
		} catch (err) {
			console.warn(
				`[sm-client] Error syncing memory ${memory.id}: ${getErrorMessage(err)}`,
			);
		}
	});

	return { queued: true };
}

/**
 * Fetch cross-project memories from Singularity Memory for a query.
 * Returns [] if SM unavailable (graceful fallback to local-only).
 *
 * Tenant gate (sf-mp723nju-2cpeoc):
 *   opts.tenant          — repoIdentity-derived tenant (preferred source)
 *   opts.tenantId        — legacy explicit tenant override
 *   opts.crossTenantEnabled — skip tenant filter (caller logged audit warning)
 *
 * Resolution order: opts.tenant > opts.tenantId > SM_TENANT_ID env var.
 * When no tenant resolves AND crossTenantEnabled is false, returns [] (safe
 * default — prevents unrestricted cross-project recall).
 */
export async function querySmMemories(query, opts = {}) {
	if (!opts.smConnected) {
		// SM not available — return empty (caller will use local memories)
		return [];
	}

	const addr =
		opts.smAddr ||
		process.env.SINGULARITY_MEMORY_ADDR ||
		"http://localhost:8080";
	const limit = opts.limit || 5; // Cross-project recall limit (smaller than local)
	const crossTenantEnabled = opts.crossTenantEnabled === true;

	// Resolve effective tenant: opts.tenant (repoIdentity) takes priority,
	// then opts.tenantId, then SM_TENANT_ID env var via resolveSmScope.
	const repoTenant =
		typeof opts.tenant === "string" && opts.tenant.trim()
			? opts.tenant.trim()
			: null;
	const scope = resolveSmScope(opts);
	const effectiveTenantId = repoTenant || scope.tenantId;

	// Require at least some tenant scope unless cross-tenant is explicitly
	// enabled — prevents unrestricted cross-project recall.
	if (!effectiveTenantId && !crossTenantEnabled) {
		return [];
	}

	try {
		const requestBody = {
			query,
			limit,
			...(scope.projectId ? { projectId: scope.projectId } : {}),
		};
		if (effectiveTenantId && !crossTenantEnabled) {
			// Send tenant filter server-side; client-side filter below is the
			// hard guarantee for misconfigured SM servers.
			requestBody.tenant = effectiveTenantId;
			requestBody.tenantId = effectiveTenantId; // Legacy SM API compat
		}

		const response = await fetch(`${addr}/v1/memories/query`, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
			},
			body: JSON.stringify(requestBody),
			signal: AbortSignal.timeout(5000),
		});

		if (!response.ok) {
			return [];
		}

		const data = await response.json();
		const memories = data.memories || [];

		// Defense-in-depth: drop foreign-tenant memories a misconfigured SM
		// server may have leaked. filterSmMemoriesToTenant logs dropped rows.
		// Skip filter when crossTenantEnabled (caller already logged audit warning).
		if (effectiveTenantId && !crossTenantEnabled) {
			return filterSmMemoriesToTenant(memories, effectiveTenantId);
		}
		return memories;
	} catch {
		// Network error or timeout — fail open
		return [];
	}
}

/**
 * Filter SM-returned memories to those that either claim the expected
 * tenant OR carry no tenant field at all (legacy).
 *
 * Purpose: enforce tenant isolation defensively after remote recall so a
 * permissive or stale SM server cannot inject another repo's memories.
 *
 * Consumer: querySmMemories before cross-project memories enter prompt ranking.
 *
 * The forgiving "no tenant = allow" rule is deliberate: SM is the
 * upstream and may legitimately store legacy unmigrated entries.
 * If you want hard-fail-closed semantics, set
 * SM_REQUIRE_TENANT_CLAIM=true.
 */
export function filterSmMemoriesToTenant(memories, expectedTenantId) {
	if (!Array.isArray(memories)) return [];
	if (!expectedTenantId) {
		// Caller didn't supply a tenant — preserve fail-open semantics
		// (caller is responsible for refusing to query in that case).
		return memories;
	}
	const requireClaim = process.env.SM_REQUIRE_TENANT_CLAIM === "true";
	const allowed = [];
	let droppedForeign = 0;
	let droppedMissingClaim = 0;
	for (const m of memories) {
		const claim = m?.tenantId ?? m?.tenant ?? null;
		if (claim === null || claim === undefined) {
			if (requireClaim) {
				droppedMissingClaim++;
				continue;
			}
			allowed.push(m);
			continue;
		}
		if (claim === expectedTenantId) {
			allowed.push(m);
		} else {
			droppedForeign++;
		}
	}
	if (droppedForeign > 0 || droppedMissingClaim > 0) {
		console.warn(
			`[sm-client] dropped ${droppedForeign} foreign-tenant and ${droppedMissingClaim} unclaimed memories (expected tenantId=${expectedTenantId})`,
		);
	}
	return allowed;
}

/**
 * Get SM client status for doctor checks.
 */
export function getSmStatus() {
	if (process.env.SM_ENABLED !== "true") {
		return {
			connected: false,
			reason: "disabled (set SM_ENABLED=true to enable)",
		};
	}

	const addr = process.env.SINGULARITY_MEMORY_ADDR || "http://localhost:8080";
	return {
		configured: true,
		addr,
		tenantConfigured: Boolean(resolveSmScope().tenantId),
		status: "unknown", // Status should be checked via initializeSmClient
	};
}

/**
 * Disable SM for testing or manual offline mode.
 */
export function disableSmClient() {
	process.env.SM_ENABLED = "false";
}

/**
 * Re-enable SM (after being disabled).
 */
export function enableSmClient() {
	delete process.env.SM_ENABLED;
}
