/**
 * Extension Discovery — resolves extension entry-point files from a directory tree.
 *
 * Supports two discovery modes:
 *   1. package.json with a `pi` manifest (authoritative, allows opt-out).
 *   2. Fallback to index.ts / index.js when no manifest is present.
 *
 * Purpose: decouple the physical layout of extensions on disk from the loader so
 * that extensions can declare their own entry points and library directories can
 * opt out of being loaded.
 *
 * Consumer: extension-registry.ts (ensureRegistryEntries), the sf-run loader, and
 * the test suite that validates symlink and manifest edge cases.
 */

import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
import { join, resolve } from "node:path";

function isExtensionFile(name: string): boolean {
	return (
		(name.endsWith(".ts") && !name.endsWith(".d.ts")) ||
		(name.endsWith(".js") && !name.endsWith(".d.js"))
	);
}

/**
 * Resolves the entry-point file(s) for a single extension directory.
 *
 * 1. If the directory contains a package.json with a `pi` manifest object,
 *    the manifest is authoritative:
 *    - `pi.extensions` array → resolve each entry relative to the directory.
 *    - `pi: {}` (no extensions) → return empty (library opt-out, e.g. cmux).
 * 2. Only when no `pi` manifest exists does it fall back to `index.ts` → `index.js`.
 *
 * Purpose: give extension authors explicit control over what gets loaded while
 * preserving backwards compatibility for simple extensions that only provide an
 * index file.
 *
 * Consumer: discoverExtensionEntryPaths() and tests that verify manifest vs fallback
 * resolution logic.
 */
export function resolveExtensionEntries(dir: string): string[] {
	const packageJsonPath = join(dir, "package.json");
	if (existsSync(packageJsonPath)) {
		try {
			const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
			if (pkg?.pi && typeof pkg.pi === "object") {
				// When a pi manifest exists, it is authoritative — don't fall through
				// to index.ts/index.js auto-detection. This allows library directories
				// (like cmux) to opt out by declaring "pi": {} with no extensions.
				const declared = pkg.pi.extensions;
				if (!Array.isArray(declared) || declared.length === 0) {
					return [];
				}
				return declared
					.filter(
						(entry: unknown): entry is string => typeof entry === "string",
					)
					.map((entry: string) => resolve(dir, entry))
					.filter((entry: string) => existsSync(entry));
			}
		} catch {
			// Ignore malformed manifests and fall back to index.ts/index.js discovery.
		}
	}

	const indexTs = join(dir, "index.ts");
	if (existsSync(indexTs)) {
		return [indexTs];
	}

	const indexJs = join(dir, "index.js");
	if (existsSync(indexJs)) {
		return [indexJs];
	}

	return [];
}

/**
 * Discovers all extension entry-point paths under an extensions directory.
 *
 * - Top-level .ts/.js files are treated as standalone extension entry points.
 * - Subdirectories are resolved via `resolveExtensionEntries()` (package.json →
 *   pi.extensions, then index.ts/index.js fallback).
 *
 * Purpose: produce a flat list of absolute entry-point paths that the loader can
 * require() in order, regardless of whether extensions are organised as files or
 * directories.
 *
 * Consumer: the sf-run loader bootstrap and integration tests that verify discovery
 * against fixture directories.
 */
export function discoverExtensionEntryPaths(extensionsDir: string): string[] {
	if (!existsSync(extensionsDir)) {
		return [];
	}

	const discovered: string[] = [];
	// Track real paths to avoid loading the same file via two names
	// (e.g. a real directory and a symlink that points to it).
	const seenRealPaths = new Set<string>();

	for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
		const entryPath = join(extensionsDir, entry.name);

		if (
			(entry.isFile() || entry.isSymbolicLink()) &&
			isExtensionFile(entry.name)
		) {
			const real = tryRealpath(entryPath);
			if (!seenRealPaths.has(real)) {
				seenRealPaths.add(real);
				discovered.push(entryPath);
			}
			continue;
		}

		if (entry.isDirectory() || entry.isSymbolicLink()) {
			const entries = resolveExtensionEntries(entryPath);
			for (const ep of entries) {
				const real = tryRealpath(ep);
				if (!seenRealPaths.has(real)) {
					seenRealPaths.add(real);
					discovered.push(ep);
				}
			}
		}
	}

	return discovered;
}

/** Resolve a real path, falling back to the original path on error (e.g. dangling symlink). */
function tryRealpath(p: string): string {
	try {
		return realpathSync.native(p);
	} catch {
		return p;
	}
}
