/**
 * NERF 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 { readdirSync, existsSync, realpathSync, Dirent } from "node:fs";
import { join, dirname, normalize } from "node:path";
import { spawnSync } from "node:child_process";
import { nativeScanNerfTree } from "./native-parser-bridge.js";
import { DIR_CACHE_MAX } from "./constants.js";
// ─── Directory Listing Cache ──────────────────────────────────────────────────
const dirEntryCache = new Map();
const dirListCache = new Map();
// ─── Native Tree Cache ────────────────────────────────────────────────────────
// When the native module is available, scan the entire .nerf/ tree in one call
// and serve directory listings from memory instead of individual readdirSync calls.
let nativeTreeCache = null;
let nativeTreeBase = null;
function getNativeTree(nerfDir) {
    if (nativeTreeCache && nativeTreeBase === nerfDir)
        return nativeTreeCache;
    const entries = nativeScanNerfTree(nerfDir);
    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 = nerfDir;
    return tree;
}
/**
 * Convert a native tree lookup into a relative key for the tree map.
 * Returns the relative path from the nerfDir, or null if the path isn't under nerfDir.
 */
function nativeTreeKey(dirPath, nerfDir) {
    if (!dirPath.startsWith(nerfDir))
        return null;
    const rel = dirPath.slice(nerfDir.length).replace(/^\//, '');
    return rel || '.';
}
function cachedReaddirWithTypes(dirPath) {
    const cached = dirEntryCache.get(dirPath);
    if (cached)
        return cached;
    // Try native tree cache for paths under .nerf/
    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 .nerf/
    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`;
}
/**
 * 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;
        // 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;
        // 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;
        // 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 NERF_ROOT_FILES = {
    PROJECT: "PROJECT.md",
    DECISIONS: "DECISIONS.md",
    QUEUE: "QUEUE.md",
    STATE: "STATE.md",
    REQUIREMENTS: "REQUIREMENTS.md",
    OVERRIDES: "OVERRIDES.md",
    KNOWLEDGE: "KNOWLEDGE.md",
};
const LEGACY_NERF_ROOT_FILES = {
    PROJECT: "project.md",
    DECISIONS: "decisions.md",
    QUEUE: "queue.md",
    STATE: "state.md",
    REQUIREMENTS: "requirements.md",
    OVERRIDES: "overrides.md",
    KNOWLEDGE: "knowledge.md",
};
// ─── NERF Root Discovery ───────────────────────────────────────────────────────
const nerfRootCache = new Map();
/** Exported for tests only — do not call in production code. */
export function _clearNerfRootCache() {
    nerfRootCache.clear();
}
/**
 * Resolve the `.nerf` directory for a given project base path.
 *
 * Probe order:
 *   1. basePath/.nerf         — fast path (common case)
 *   2. git rev-parse root    — handles cwd-is-a-subdirectory
 *   3. Walk up from basePath — handles moved .nerf in an ancestor (bounded by git root)
 *   4. basePath/.nerf         — creation fallback (init scenario)
 *
 * Result is cached per basePath for the process lifetime.
 */
export function nerfRoot(basePath) {
    const cached = nerfRootCache.get(basePath);
    if (cached)
        return cached;
    const result = probeNerfRoot(basePath);
    nerfRootCache.set(basePath, result);
    return result;
}
function probeNerfRoot(rawBasePath) {
    // 1. Fast path — check the input path directly
    const local = join(rawBasePath, ".nerf");
    if (existsSync(local))
        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;
    }
    // 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, ".nerf");
        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, ".nerf");
            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(nerfRoot(basePath), "milestones");
}
export function resolveRuntimeFile(basePath) {
    return join(nerfRoot(basePath), "RUNTIME.md");
}
export function resolveNerfRootFile(basePath, key) {
    const root = nerfRoot(basePath);
    const canonical = join(root, NERF_ROOT_FILES[key]);
    if (existsSync(canonical))
        return canonical;
    const legacy = join(root, LEGACY_NERF_ROOT_FILES[key]);
    if (existsSync(legacy))
        return legacy;
    return canonical;
}
export function relNerfRootFile(key) {
    return `.nerf/${NERF_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 — .nerf/milestones/...) ────────────
/**
 * Build relative .nerf/ 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 `.nerf/milestones/${dir}`;
    return `.nerf/milestones/${milestoneId}`;
}
/**
 * Build relative .nerf/ 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}`;
    }
    return `${mRel}/${buildMilestoneFileName(milestoneId, suffix)}`;
}
/**
 * Build relative .nerf/ 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 .nerf/ 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 .nerf/ 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)}`;
}
