// data-dir-discovery.ts — Cwd-tolerant data dir discovery for project stores.
//
// Background: projectDataDir() returns a relative path "data/projects/<id>".
// All store reads thus depend on process.cwd(). When the MCP server is launched
// from a directory other than the one used at init_project time, stores cannot
// see existing data on disk and report the project as missing.
//
// This module resolves the data dir for a projectId once per session by
// walking up from cwd and from the registered projectPath, anchoring the
// search on `knowledge.json` (the canonical signal that a directory truly
// belongs to the given project — not just a co-located hash collision in an
// unrelated repo). Once resolved, the same base dir is used for every store
// (specs.json, knowledge.json, etc.) so they stay coherent.
//
// Reads are read-only and best-effort: writes still go through projectDataDir().
import { stat } from 'node:fs/promises';
import { join, dirname, isAbsolute } from 'node:path';
import { getProjects } from './global-projects-store.js';
const MAX_WALK_DEPTH = 8;
/** The file used to anchor discovery: presence == this dir owns the project. */
const ANCHOR_FILE = 'knowledge.json';
const dataDirCache = new Map();
/** Clear the resolution cache — TEST USE ONLY. */
export function clearDataDirCache() {
    dataDirCache.clear();
}
async function fileExists(filePath) {
    try {
        const s = await stat(filePath);
        return s.isFile();
    }
    catch {
        return false;
    }
}
/**
 * Walk upward from `startDir` looking for the first directory under which
 * `<dir>/<subPath>/<ANCHOR_FILE>` exists. Returns the matched data dir
 * (`<dir>/<subPath>`), or null when no anchor is found.
 */
async function walkUpForAnchor(startDir, subPath) {
    if (!isAbsolute(startDir)) {
        return null;
    }
    let current = startDir;
    for (let i = 0; i < MAX_WALK_DEPTH; i++) {
        const dataDir = join(current, subPath);
        if (await fileExists(join(dataDir, ANCHOR_FILE))) {
            return dataDir;
        }
        const parent = dirname(current);
        if (parent === current) {
            return null;
        }
        current = parent;
    }
    return null;
}
/**
 * Resolve the data dir for `projectId`, walking ancestors of cwd and of the
 * registered projectPath when the cwd-relative dir lacks the anchor file.
 *
 * Result is cached per session: the first successful (or unsuccessful)
 * resolution sticks, so repeated reads do not pay the I/O cost.
 *
 * Returns null when no `knowledge.json` is found anywhere — callers should
 * treat that as "project not initialized in any reachable location".
 *
 * @param projectId Project hash identifier.
 * @param cwd       Optional working directory override (defaults to
 *                  `process.cwd()`). Exposed for tests, where Vitest workers
 *                  cannot mutate the real process cwd.
 */
export async function resolveProjectDataDir(projectId, cwd = process.cwd()) {
    if (dataDirCache.has(projectId)) {
        return dataDirCache.get(projectId) ?? null;
    }
    const subPath = join('data', 'projects', projectId);
    // 1. cwd-relative direct hit (current behavior, fast path)
    const direct = join(cwd, subPath);
    if (await fileExists(join(direct, ANCHOR_FILE))) {
        dataDirCache.set(projectId, direct);
        return direct;
    }
    // 2. Walk up from cwd
    const fromCwd = await walkUpForAnchor(cwd, subPath);
    if (fromCwd !== null) {
        dataDirCache.set(projectId, fromCwd);
        return fromCwd;
    }
    // 3. Use the registered projectPath, if any
    let registeredPath = null;
    try {
        const projects = await getProjects();
        const match = projects.find((p) => p.hash === projectId);
        if (match) {
            registeredPath = match.path;
        }
    }
    catch {
        /* registry unreadable — skip */
    }
    if (registeredPath !== null) {
        const directFromRegistered = join(registeredPath, subPath);
        if (await fileExists(join(directFromRegistered, ANCHOR_FILE))) {
            dataDirCache.set(projectId, directFromRegistered);
            return directFromRegistered;
        }
        const fromRegistered = await walkUpForAnchor(registeredPath, subPath);
        if (fromRegistered !== null) {
            dataDirCache.set(projectId, fromRegistered);
            return fromRegistered;
        }
    }
    dataDirCache.set(projectId, null);
    return null;
}
/**
 * Discover an existing data file for `projectId`. Returns the absolute path
 * when the resolved data dir contains `<relative>`, or null otherwise.
 *
 * Anchored on `knowledge.json` — if no project owns the dir, no file is
 * returned even if a same-named file happens to live in another repo's
 * `data/projects/<id>/` tree.
 *
 * @param projectId Project hash identifier.
 * @param relative  Path under `data/projects/<id>/`, e.g. "specs.json".
 */
export async function discoverProjectDataFile(projectId, relative, cwd) {
    const dir = await resolveProjectDataDir(projectId, cwd ?? process.cwd());
    if (dir === null) {
        return null;
    }
    const candidate = join(dir, relative);
    if (await fileExists(candidate)) {
        return candidate;
    }
    return null;
}
//# sourceMappingURL=data-dir-discovery.js.map