// SF — Ecosystem extension loader for ./.sf/extensions/
// Discovers and registers single-file extensions that consume SFExtensionAPI.
// Trust-gated (mirrors pi's `.pi/extensions/` model) and isolated from pi's
// own loader chain — handlers run in SF's own dispatch step, not pi's.
import * as fs from "node:fs";
import * as path from "node:path";
import { pathToFileURL } from "node:url";
import { getAgentDir } from "@singularity-forge/coding-agent";
import { getErrorMessage } from "../error-utils.js";
import { logWarning } from "../workflow-logger.js";
import { createSFExtensionAPI } from "./sf-extension-api.js";

// ─── Trust check (inlined; pi does not export isProjectTrusted from its
// package root, and constraint forbids modifying packages/coding-agent/) ─
const TRUSTED_PROJECTS_FILE = "trusted-projects.json";
function isProjectTrusted(projectPath, agentDir) {
	const canonical = path.resolve(projectPath);
	const trustedPath = path.join(agentDir, TRUSTED_PROJECTS_FILE);
	try {
		const content = fs.readFileSync(trustedPath, "utf-8");
		const parsed = JSON.parse(content);
		if (Array.isArray(parsed)) {
			return parsed.includes(canonical);
		}
	} catch {
		// missing or malformed — treat as untrusted
	}
	return false;
}
// ─── Ready-promise singleton ────────────────────────────────────────────
let _readyPromise = null;
let _untrustedWarned = false;
/**
 * Discover and register ecosystem extensions from `./.sf/extensions/`.
 * Idempotent: subsequent calls with the same arguments return the same
 * pending promise (no double-load).
 */
export function loadEcosystemExtensions(
	pi,
	sharedHandlers,
	cwd = process.cwd(),
) {
	if (_readyPromise) return _readyPromise;
	_readyPromise = _loadEcosystemExtensionsImpl(pi, sharedHandlers, cwd);
	return _readyPromise;
}
/**
 * Returns a promise that resolves when ecosystem loading has completed.
 * If loading was never kicked off this returns a resolved promise so the
 * `before_agent_start` handler can `await` unconditionally.
 */
export function getEcosystemReadyPromise() {
	return _readyPromise ?? Promise.resolve();
}
/** Test-only: clear the singleton so tests can re-run loading. */
export function _resetEcosystemLoader() {
	_readyPromise = null;
	_untrustedWarned = false;
}
// ─── Implementation ─────────────────────────────────────────────────────
async function _loadEcosystemExtensionsImpl(pi, sharedHandlers, cwd) {
	const extDir = path.join(cwd, ".sf", "extensions");
	if (!fs.existsSync(extDir)) return;
	// Trust gate: refuse to load arbitrary code from untrusted project dirs.
	if (!isProjectTrusted(cwd, getAgentDir())) {
		if (!_untrustedWarned) {
			_untrustedWarned = true;
			logWarning(
				"ecosystem",
				".sf/extensions present but project is not trusted — skipping ecosystem extensions. Run `pi trust` to opt in.",
			);
		}
		return;
	}
	// Resolve realpath ONCE so symlink-escape detection has a stable anchor.
	let realExtDir;
	try {
		realExtDir = fs.realpathSync(extDir);
	} catch (err) {
		logWarning(
			"ecosystem",
			`failed to resolve extensions dir: ${getErrorMessage(err)}`,
		);
		return;
	}
	let entries;
	try {
		entries = fs
			.readdirSync(extDir)
			.filter((f) => f.endsWith(".js") || f.endsWith(".ts"))
			.sort(); // deterministic load order
	} catch (err) {
		logWarning(
			"ecosystem",
			`failed to read extensions dir: ${getErrorMessage(err)}`,
		);
		return;
	}
	// The wrapper api is built once per loader run and shared by all extensions
	// so they all read from the same module-level snapshot.
	const api = createSFExtensionAPI(pi, sharedHandlers);
	for (const entry of entries) {
		await _loadOne(extDir, realExtDir, entry, api);
	}
}
async function _loadOne(extDir, realExtDir, entry, api) {
	const fullPath = path.join(extDir, entry);
	// Symlink-escape guard: reject entries whose realpath is not under realExtDir.
	let realFullPath;
	try {
		realFullPath = fs.realpathSync(fullPath);
	} catch (err) {
		logWarning(
			"ecosystem",
			`failed to resolve ${entry}: ${getErrorMessage(err)}`,
		);
		return;
	}
	const realExtDirWithSep = realExtDir.endsWith(path.sep)
		? realExtDir
		: realExtDir + path.sep;
	if (
		realFullPath !== realExtDir &&
		!realFullPath.startsWith(realExtDirWithSep)
	) {
		logWarning(
			"ecosystem",
			`rejecting ${entry}: realpath escapes extensions dir`,
		);
		return;
	}
	// For .ts files, require a sibling compiled .js — we do not run a TS loader
	// in production. Drop mtime heuristics: if .js exists, prefer it; otherwise warn.
	let importPath = realFullPath;
	if (entry.endsWith(".ts")) {
		const jsSibling = realFullPath.slice(0, -3) + ".js";
		if (fs.existsSync(jsSibling)) {
			importPath = jsSibling;
		} else {
			logWarning(
				"ecosystem",
				`${entry}: TypeScript source has no compiled .js sibling — compile it first`,
			);
			return;
		}
	}
	let mod;
	try {
		mod = await import(pathToFileURL(importPath).href);
	} catch (err) {
		logWarning(
			"ecosystem",
			`failed to import ${entry}: ${getErrorMessage(err)}`,
		);
		return;
	}
	const factory = mod?.default;
	if (typeof factory !== "function") {
		logWarning("ecosystem", `${entry}: default export is not a function`);
		return;
	}
	try {
		await factory(api);
	} catch (err) {
		logWarning(
			"ecosystem",
			`factory threw for ${entry}: ${getErrorMessage(err)}`,
		);
	}
}
