/**
 * SF Paths — ID-based path resolution
 *
 * Directories use bare IDs: M001/, S01/, etc.
 * Files use ID-SUFFIX: M001-ROADMAP.md, S01-PLAN.md, T01-PLAN.md
 *
 * Resolvers still handle legacy descriptor-suffixed names
 * (e.g. M001-FLIGHT-SIMULATOR/, T03-INSTALL-PACKAGES-PLAN.md)
 * via prefix matching, so existing projects work without migration.
 */
import { spawnSync } from "node:child_process";
import {
	Dirent,
	existsSync,
	readdirSync,
	readFileSync,
	realpathSync,
} from "node:fs";
import { dirname, join, normalize } from "node:path";
import { DIR_CACHE_MAX } from "./constants.js";
import { nativeScanSfTree } from "./native-parser-bridge.js";
import { sfHome } from "./sf-home.js";

// ─── Directory Listing Cache ──────────────────────────────────────────────────
const dirEntryCache = new Map();
const dirListCache = new Map();
// ─── Native Tree Cache ────────────────────────────────────────────────────────
// When the native module is available, scan the entire .sf/ tree in one call
// and serve directory listings from memory instead of individual readdirSync calls.
let nativeTreeCache = null;
let nativeTreeBase = null;
function _getNativeTree(sfDir) {
	if (nativeTreeCache && nativeTreeBase === sfDir) return nativeTreeCache;
	const entries = nativeScanSfTree(sfDir);
	if (!entries) return null;
	// Build a map of parent directory -> entries
	const tree = new Map();
	for (const entry of entries) {
		const parts = entry.path.split("/");
		const parentPath = parts.slice(0, -1).join("/");
		const parentKey = parentPath || ".";
		if (!tree.has(parentKey)) tree.set(parentKey, []);
		tree.get(parentKey).push(entry);
	}
	nativeTreeCache = tree;
	nativeTreeBase = sfDir;
	return tree;
}
/**
 * Convert a native tree lookup into a relative key for the tree map.
 * Returns the relative path from the sfDir, or null if the path isn't under sfDir.
 */
function nativeTreeKey(dirPath, sfDir) {
	if (!dirPath.startsWith(sfDir)) return null;
	const rel = dirPath.slice(sfDir.length).replace(/^\//, "");
	return rel || ".";
}
function cachedReaddirWithTypes(dirPath) {
	const cached = dirEntryCache.get(dirPath);
	if (cached) return cached;
	// Try native tree cache for paths under .sf/
	if (nativeTreeBase) {
		const key = nativeTreeKey(dirPath, nativeTreeBase);
		if (key && nativeTreeCache) {
			const treeEntries = nativeTreeCache.get(key);
			if (treeEntries) {
				// Synthesize Dirent-like objects from native tree entries
				const dirents = treeEntries.map((e) => {
					const d = Object.create(Dirent.prototype);
					Object.assign(d, {
						name: e.name,
						parentPath: dirPath,
						path: dirPath,
					});
					// Override the type check methods
					const isDir = e.isDir;
					d.isDirectory = () => isDir;
					d.isFile = () => !isDir;
					d.isSymbolicLink = () => false;
					d.isBlockDevice = () => false;
					d.isCharacterDevice = () => false;
					d.isFIFO = () => false;
					d.isSocket = () => false;
					return d;
				});
				if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
				dirEntryCache.set(dirPath, dirents);
				return dirents;
			}
		}
	}
	const entries = readdirSync(dirPath, { withFileTypes: true });
	if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
	dirEntryCache.set(dirPath, entries);
	return entries;
}
function cachedReaddir(dirPath) {
	const cached = dirListCache.get(dirPath);
	if (cached) return cached;
	// Try native tree cache for paths under .sf/
	if (nativeTreeBase) {
		const key = nativeTreeKey(dirPath, nativeTreeBase);
		if (key && nativeTreeCache) {
			const treeEntries = nativeTreeCache.get(key);
			if (treeEntries) {
				const names = treeEntries.map((e) => e.name);
				if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
				dirListCache.set(dirPath, names);
				return names;
			}
		}
	}
	const entries = readdirSync(dirPath);
	if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
	dirListCache.set(dirPath, entries);
	return entries;
}
/**
 * Clear the directory listing cache.
 * Call after milestone transitions, file creation in planning directories,
 * or at the start/end of a dispatch cycle.
 */
export function clearPathCache() {
	dirEntryCache.clear();
	dirListCache.clear();
	nativeTreeCache = null;
	nativeTreeBase = null;
}
// ─── Name Builders ─────────────────────────────────────────────────────────
/**
 * Build a milestone-level file name.
 * ("M001", "CONTEXT") → "M001-CONTEXT.md"
 */
export function buildMilestoneFileName(milestoneId, suffix) {
	return `${milestoneId}-${suffix}.md`;
}
function canonicalMilestoneIdForDir(milestoneId, milestoneDir) {
	const dirName = milestoneDir ? milestoneDir.split(/[/\\]/).pop() : null;
	const canonicalPrefix = milestoneId.match(/^([A-Z]\d{3})-/)?.[1];
	return canonicalPrefix && dirName === canonicalPrefix
		? canonicalPrefix
		: milestoneId;
}
/**
 * Build a slice-level file name.
 * ("S01", "PLAN") → "S01-PLAN.md"
 */
export function buildSliceFileName(sliceId, suffix) {
	return `${sliceId}-${suffix}.md`;
}
/**
 * Build a task file name.
 * ("T03", "PLAN") → "T03-PLAN.md"
 * ("T03", "SUMMARY") → "T03-SUMMARY.md"
 */
export function buildTaskFileName(taskId, suffix) {
	return `${taskId}-${suffix}.md`;
}
// ─── Resolvers ─────────────────────────────────────────────────────────────
/**
 * Find a directory entry by ID prefix within a parent directory.
 * Exact match first (M001), then prefix match (M001-SOMETHING) for
 * backward compatibility with legacy descriptor directories.
 * Returns the full directory name or null.
 */
export function resolveDir(parentDir, idPrefix) {
	if (!existsSync(parentDir)) return null;
	try {
		const entries = cachedReaddirWithTypes(parentDir);
		// Exact match first (current convention: bare ID)
		const exact = entries.find((e) => e.isDirectory() && e.name === idPrefix);
		if (exact) return exact.name;
		// Unique-ID fallback: M001-abc123 should still resolve bare M001/
		const canonicalPrefix = idPrefix.match(/^([A-Z]\d{3})-/)?.[1];
		if (canonicalPrefix) {
			const canonical = entries.find(
				(e) => e.isDirectory() && e.name === canonicalPrefix,
			);
			if (canonical) return canonical.name;
		}
		// Prefix match for legacy descriptor dirs: M001-SOMETHING
		const prefixed = entries.find(
			(e) => e.isDirectory() && e.name.startsWith(idPrefix + "-"),
		);
		return prefixed ? prefixed.name : null;
	} catch {
		return null;
	}
}
/**
 * Find a file by ID prefix and suffix within a directory.
 * Checks in order:
 *   1. Direct: ID-SUFFIX.md (e.g. M001-ROADMAP.md, T03-PLAN.md)
 *   2. Legacy descriptor: ID-DESCRIPTOR-SUFFIX.md (e.g. T03-INSTALL-PACKAGES-PLAN.md)
 *   3. Legacy bare: suffix.md (e.g. roadmap.md)
 */
export function resolveFile(dir, idPrefix, suffix) {
	if (!existsSync(dir)) return null;
	const target = `${idPrefix}-${suffix}.md`.toUpperCase();
	try {
		const entries = cachedReaddir(dir);
		// Direct match: ID-SUFFIX.md
		const direct = entries.find((e) => e.toUpperCase() === target);
		if (direct) return direct;
		// Unique-ID fallback: M001-abc123-CONTEXT.md should still resolve M001-CONTEXT.md
		const canonicalPrefix = idPrefix.match(/^([A-Z]\d{3})-/)?.[1];
		if (canonicalPrefix) {
			const canonicalTarget = `${canonicalPrefix}-${suffix}.md`.toUpperCase();
			const canonicalDirect = entries.find(
				(e) => e.toUpperCase() === canonicalTarget,
			);
			if (canonicalDirect) return canonicalDirect;
		}
		// Legacy pattern match: ID-DESCRIPTOR-SUFFIX.md
		const pattern = new RegExp(`^${idPrefix}-.*-${suffix}\\.md$`, "i");
		const match = entries.find((e) => pattern.test(e));
		if (match) return match;
		if (canonicalPrefix) {
			const canonicalPattern = new RegExp(
				`^${canonicalPrefix}-.*-${suffix}\\.md$`,
				"i",
			);
			const canonicalMatch = entries.find((e) => canonicalPattern.test(e));
			if (canonicalMatch) return canonicalMatch;
		}
		// Legacy fallback: suffix.md
		const legacy = entries.find(
			(e) => e.toLowerCase() === `${suffix.toLowerCase()}.md`,
		);
		if (legacy) return legacy;
		return null;
	} catch {
		return null;
	}
}
/**
 * Find all task files matching a pattern in a tasks directory.
 * Returns sorted file names matching T##-SUFFIX.md or legacy T##-*-SUFFIX.md
 */
export function resolveTaskFiles(tasksDir, suffix) {
	if (!existsSync(tasksDir)) return [];
	try {
		// Current convention: T01-PLAN.md
		const currentPattern = new RegExp(`^T\\d+-${suffix}\\.md$`, "i");
		// Legacy convention: T01-INSTALL-PACKAGES-PLAN.md
		const legacyPattern = new RegExp(`^T\\d+-.*-${suffix}\\.md$`, "i");
		return cachedReaddir(tasksDir)
			.filter((f) => currentPattern.test(f) || legacyPattern.test(f))
			.sort();
	} catch {
		return [];
	}
}
/**
 * Find all task JSON files matching a pattern in a tasks directory.
 * Returns sorted file names matching T##-SUFFIX.json or legacy T##-*-SUFFIX.json
 */
export function resolveTaskJsonFiles(tasksDir, suffix) {
	if (!existsSync(tasksDir)) return [];
	try {
		const currentPattern = new RegExp(`^T\\d+-${suffix}\\.json$`, "i");
		const legacyPattern = new RegExp(`^T\\d+-.*-${suffix}\\.json$`, "i");
		return cachedReaddir(tasksDir)
			.filter((f) => currentPattern.test(f) || legacyPattern.test(f))
			.sort();
	} catch {
		return [];
	}
}
// ─── Full Path Builders ────────────────────────────────────────────────────
export const SF_ROOT_FILES = {
	PROJECT: "PROJECT.md",
	DECISIONS: "DECISIONS.md",
	QUEUE: "QUEUE.md",
	STATE: "STATE.md",
	REQUIREMENTS: "REQUIREMENTS.md",
	OVERRIDES: "OVERRIDES.md",
	KNOWLEDGE: "KNOWLEDGE.md",
	CODEBASE: "CODEBASE.md",
};
const LEGACY_SF_ROOT_FILES = {
	PROJECT: "project.md",
	DECISIONS: "decisions.md",
	QUEUE: "queue.md",
	STATE: "state.md",
	REQUIREMENTS: "requirements.md",
	OVERRIDES: "overrides.md",
	KNOWLEDGE: "knowledge.md",
	CODEBASE: "codebase.md",
};
// ─── SF Root Discovery ───────────────────────────────────────────────────────
const sfRootCache = new Map();
/** Exported for tests only — do not call in production code. */
export function _clearSfRootCache() {
	sfRootCache.clear();
}
/**
 * Resolve the `.sf` directory for a given project base path.
 *
 * Probe order:
 *   1. basePath/.sf         — fast path (common case)
 *   2. git rev-parse root    — handles cwd-is-a-subdirectory
 *   3. Walk up from basePath — handles moved .sf in an ancestor (bounded by git root)
 *   4. basePath/.sf         — creation fallback (init scenario)
 *
 * Result is cached per basePath for the process lifetime.
 */
export function sfRoot(basePath) {
	const cached = sfRootCache.get(basePath);
	if (cached) return cached;
	const result = probeSfRoot(basePath);
	sfRootCache.set(basePath, result);
	return result;
}
export const projectRoot = sfRoot;
// ─── Self-Detection & Runtime Root ───────────────────────────────────────────
let _isRunningOnSelfCache = null;
/**
 * Detect whether SF is running on its own source tree. When true, runtime
 * self-reporting (notifications, activity, journal, self-feedback, etc.) is
 * redirected to `~/.sf/` instead of `<basePath>/.sf/` so that feedback ABOUT
 * SF as a tool accumulates at the global level rather than polluting the
 * forge repo with per-project runtime artifacts.
 *
 * Detection signals (must match BOTH for true):
 *   1. `<basePath>/package.json` exists with `"name": "singularity-forge"`
 *   2. `<basePath>/src/resources/extensions/sf/loader.ts` exists
 *
 * Cached on first call per basePath to avoid repeat filesystem hits.
 */
export function isRunningOnSelf(basePath) {
	if (_isRunningOnSelfCache?.basePath === basePath) {
		return _isRunningOnSelfCache.result;
	}
	let result = false;
	try {
		const pkgPath = join(basePath, "package.json");
		if (existsSync(pkgPath)) {
			const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
			if (pkg?.name === "singularity-forge") {
				const loaderPath = join(
					basePath,
					"src/resources/extensions/sf/loader.ts",
				);
				if (existsSync(loaderPath)) {
					result = true;
				}
			}
		}
	} catch {
		// Detection failure → false (default to per-repo .sf/)
	}
	_isRunningOnSelfCache = { basePath, result };
	return result;
}
/** Reset the self-detection cache. Test-only. */
export function _resetSelfDetectionCache() {
	_isRunningOnSelfCache = null;
}
/**
 * Resolve the directory that holds SF runtime self-reporting artifacts:
 * notifications.jsonl, activity/, journal/, self-feedback.jsonl,
 * routing-history.json, metrics.json, event-log.jsonl, forensics/, audit/,
 * exec/, model-benchmarks/, reports/, repo-meta.json.
 *
 * Default: `<basePath>/.sf` (same as sfRoot).
 * When isRunningOnSelf(basePath) returns true: `~/.sf` (so SF self-development
 * feedback lands at the global level, not in the singularity-forge tree).
 *
 * IMPORTANT: tracked artifacts (PROJECT.md, DECISIONS.md, REQUIREMENTS.md,
 * QUEUE.md, milestones/, KNOWLEDGE.md) MUST continue to use sfRoot(basePath)
 * — they are durable project memory per ADR-001 and remain in the repo.
 */
export function sfRuntimeRoot(basePath) {
	if (isRunningOnSelf(basePath)) return sfHome();
	return sfRoot(basePath);
}
/**
 * Detect if a path is inside a .sf/worktrees/<name>/ structure.
 *
 * SF auto-worktrees live at <project>/.sf/worktrees/<milestoneId>/.
 * When sfRoot() is called with such a path, we must NOT walk up to the
 * project root's .sf — each worktree manages its own .sf state (#2594).
 *
 * Matches both forward-slash and platform-native separators to handle
 * Windows paths (path.sep = '\\') and normalized Unix paths.
 */
function isInsideSfWorktree(p) {
	// Match /.sf/worktrees/<name> where <name> is the final segment or
	// followed by a separator. The <name> segment must be non-empty.
	const sepFwd = "/";
	const sepNative = "\\";
	const markers = [
		`${sepFwd}.sf${sepFwd}worktrees${sepFwd}`,
		`${sepNative}.sf${sepNative}worktrees${sepNative}`,
	];
	for (const marker of markers) {
		const idx = p.indexOf(marker);
		if (idx === -1) continue;
		// Verify there's a non-empty worktree name after the marker
		const afterMarker = p.slice(idx + marker.length);
		// The name is everything up to the next separator (or end of string)
		const nameEnd = afterMarker.search(/[/\\]/);
		const name = nameEnd === -1 ? afterMarker : afterMarker.slice(0, nameEnd);
		if (name.length > 0) return true;
	}
	return false;
}
function probeSfRoot(rawBasePath) {
	// 1. Fast path — check the input path directly
	const local = join(rawBasePath, ".sf");
	if (existsSync(local)) return local;
	// 1b. Worktree guard (#2594) — if basePath is inside a .sf/worktrees/<name>/
	//     structure, return the worktree-local .sf path immediately. Without this,
	//     the git-root probe (step 2) or walk-up (step 3) escapes to the project
	//     root's .sf, causing ensurePreconditions() and deriveState() to read/write
	//     state in the wrong location.
	if (isInsideSfWorktree(rawBasePath)) return local;
	// Resolve symlinks so path comparisons work correctly across platforms
	// (e.g. macOS /var → /private/var). Use rawBasePath as fallback if not resolvable.
	let basePath;
	try {
		basePath = realpathSync.native(rawBasePath);
	} catch {
		basePath = rawBasePath;
	}
	// Also check the resolved path for the worktree pattern (macOS /tmp → /private/tmp)
	if (basePath !== rawBasePath && isInsideSfWorktree(basePath)) return local;
	// 2. Git root anchor — used as both probe target and walk-up boundary
	//    Only walk if we're inside a git project — prevents escaping into
	//    unrelated filesystem territory when running outside any repo.
	let gitRoot = null;
	try {
		const out = spawnSync("git", ["rev-parse", "--show-toplevel"], {
			cwd: basePath,
			encoding: "utf-8",
		});
		if (out.status === 0) {
			const r = out.stdout.trim();
			if (r) gitRoot = normalize(r);
		}
	} catch {
		/* git not available */
	}
	if (gitRoot) {
		const candidate = join(gitRoot, ".sf");
		if (existsSync(candidate)) return candidate;
	}
	// 3. Walk up from basePath to the git root (only if we are in a subdirectory)
	if (gitRoot && basePath !== gitRoot) {
		let cur = dirname(basePath);
		while (cur !== basePath) {
			const candidate = join(cur, ".sf");
			if (existsSync(candidate)) return candidate;
			if (cur === gitRoot) break;
			basePath = cur;
			cur = dirname(cur);
		}
	}
	// 4. Fallback for init/creation
	return local;
}
export function milestonesDir(basePath) {
	return join(sfRoot(basePath), "milestones");
}
export function resolveRuntimeFile(basePath) {
	return join(sfRoot(basePath), "RUNTIME.md");
}
export function resolveSfRootFile(basePath, key) {
	const root = sfRoot(basePath);
	const canonical = join(root, SF_ROOT_FILES[key]);
	if (existsSync(canonical)) return canonical;
	const legacy = join(root, LEGACY_SF_ROOT_FILES[key]);
	if (existsSync(legacy)) return legacy;
	return canonical;
}
export function relSfRootFile(key) {
	return `.sf/${SF_ROOT_FILES[key]}`;
}
/**
 * Resolve the full path to a milestone directory.
 * Returns null if the milestone doesn't exist.
 */
export function resolveMilestonePath(basePath, milestoneId) {
	const dir = resolveDir(milestonesDir(basePath), milestoneId);
	return dir ? join(milestonesDir(basePath), dir) : null;
}
/**
 * Resolve the full path to a milestone file (e.g. ROADMAP, CONTEXT, RESEARCH).
 */
export function resolveMilestoneFile(basePath, milestoneId, suffix) {
	const mDir = resolveMilestonePath(basePath, milestoneId);
	if (!mDir) return null;
	const file = resolveFile(mDir, milestoneId, suffix);
	return file ? join(mDir, file) : null;
}
/**
 * Resolve the full path to a slice directory within a milestone.
 */
export function resolveSlicePath(basePath, milestoneId, sliceId) {
	const mDir = resolveMilestonePath(basePath, milestoneId);
	if (!mDir) return null;
	const slicesDir = join(mDir, "slices");
	const dir = resolveDir(slicesDir, sliceId);
	return dir ? join(slicesDir, dir) : null;
}
/**
 * Resolve the full path to a slice file (e.g. PLAN, RESEARCH, CONTEXT, SUMMARY).
 */
export function resolveSliceFile(basePath, milestoneId, sliceId, suffix) {
	const sDir = resolveSlicePath(basePath, milestoneId, sliceId);
	if (!sDir) return null;
	const file = resolveFile(sDir, sliceId, suffix);
	return file ? join(sDir, file) : null;
}
/**
 * Resolve the tasks directory within a slice.
 */
export function resolveTasksDir(basePath, milestoneId, sliceId) {
	const sDir = resolveSlicePath(basePath, milestoneId, sliceId);
	if (!sDir) return null;
	const tDir = join(sDir, "tasks");
	return existsSync(tDir) ? tDir : null;
}
/**
 * Resolve a specific task file.
 */
export function resolveTaskFile(
	basePath,
	milestoneId,
	sliceId,
	taskId,
	suffix,
) {
	const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
	if (!tDir) return null;
	const file = resolveFile(tDir, taskId, suffix);
	return file ? join(tDir, file) : null;
}
// ─── Relative Path Builders (for prompts — .sf/milestones/...) ────────────
/**
 * Build relative .sf/ path to a milestone directory.
 * Uses the actual directory name on disk if it exists, otherwise bare ID.
 */
export function relMilestonePath(basePath, milestoneId) {
	const dir = resolveDir(milestonesDir(basePath), milestoneId);
	if (dir) return `.sf/milestones/${dir}`;
	return `.sf/milestones/${milestoneId}`;
}
/**
 * Build relative .sf/ path to a milestone file.
 */
export function relMilestoneFile(basePath, milestoneId, suffix) {
	const mRel = relMilestonePath(basePath, milestoneId);
	const mDir = resolveMilestonePath(basePath, milestoneId);
	if (mDir) {
		const file = resolveFile(mDir, milestoneId, suffix);
		if (file) return `${mRel}/${file}`;
	}
	const fileId = canonicalMilestoneIdForDir(milestoneId, mDir);
	return `${mRel}/${buildMilestoneFileName(fileId, suffix)}`;
}
/**
 * Build relative .sf/ path to a slice directory.
 */
export function relSlicePath(basePath, milestoneId, sliceId) {
	const mRel = relMilestonePath(basePath, milestoneId);
	const mDir = resolveMilestonePath(basePath, milestoneId);
	if (mDir) {
		const slicesDir = join(mDir, "slices");
		const dir = resolveDir(slicesDir, sliceId);
		if (dir) return `${mRel}/slices/${dir}`;
	}
	return `${mRel}/slices/${sliceId}`;
}
/**
 * Build relative .sf/ path to a slice file.
 */
export function relSliceFile(basePath, milestoneId, sliceId, suffix) {
	const sRel = relSlicePath(basePath, milestoneId, sliceId);
	const sDir = resolveSlicePath(basePath, milestoneId, sliceId);
	if (sDir) {
		const file = resolveFile(sDir, sliceId, suffix);
		if (file) return `${sRel}/${file}`;
	}
	return `${sRel}/${buildSliceFileName(sliceId, suffix)}`;
}
/**
 * Build relative .sf/ path to a task file.
 */
export function relTaskFile(basePath, milestoneId, sliceId, taskId, suffix) {
	const sRel = relSlicePath(basePath, milestoneId, sliceId);
	const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
	if (tDir) {
		const file = resolveFile(tDir, taskId, suffix);
		if (file) return `${sRel}/tasks/${file}`;
	}
	return `${sRel}/tasks/${buildTaskFileName(taskId, suffix)}`;
}
