import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
/**
 * Resolve the InDusk project root by walking up from the given directory
 * until `.indusk/config.json` is found. Returns the directory containing
 * `.indusk/config.json`, or `null` if none is found up to the filesystem
 * root.
 *
 * `.indusk/config.json` is the authoritative "this is an InDusk project"
 * marker — created by `indusk init`, never by sub-apps that happen to
 * have their own `.claude/` scaffolding. Walking up to find it prevents
 * bugs like `indusk update` syncing to the wrong `.claude/` when the user
 * runs it from a sub-directory (e.g. `apps/indusk-mcp/`).
 *
 * For `indusk init` itself, use the raw cwd — init creates the marker, so
 * walk-up would either find nothing or (worse) match an ancestor project
 * the user doesn't intend to re-init.
 */
export function resolveProjectRoot(startDir) {
    let dir = startDir;
    for (let i = 0; i < 20; i++) {
        if (existsSync(join(dir, ".indusk/config.json")))
            return dir;
        const parent = resolve(dir, "..");
        if (parent === dir)
            return null;
        dir = parent;
    }
    return null;
}
/**
 * True if the given extension is listed in `.indusk/config.json`'s
 * `disabled_extensions` array. Single source of truth for the required-
 * by-default escape hatch.
 */
export function isExtensionExplicitlyDisabled(projectRoot, name) {
    const config = readConfig(projectRoot);
    const list = config?.disabled_extensions;
    if (!Array.isArray(list))
        return false;
    return list.includes(name);
}
const CONFIG_PATH = ".indusk/config.json";
export function getConfigPath(projectRoot) {
    return join(projectRoot, CONFIG_PATH);
}
export function readConfig(projectRoot) {
    const configPath = getConfigPath(projectRoot);
    if (!existsSync(configPath))
        return null;
    return JSON.parse(readFileSync(configPath, "utf-8"));
}
export function writeConfig(projectRoot, config) {
    const configPath = getConfigPath(projectRoot);
    mkdirSync(dirname(configPath), { recursive: true });
    writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`);
}
export function getPlanningDir(projectRoot) {
    const newPath = join(projectRoot, ".indusk/planning");
    const legacyPath = join(projectRoot, "planning");
    // Prefer .indusk/planning, fall back to legacy planning/ for migration
    if (existsSync(newPath))
        return newPath;
    if (existsSync(legacyPath))
        return legacyPath;
    // Default to new path (will be created by init)
    return newPath;
}
/**
 * Sanitize a string into a valid Graphiti group id.
 *
 * Graphiti uses RediSearch under the hood, which treats `-` as a token separator.
 * A query like `chitin-sportsbook` parses as "find chitin, exclude sportsbook" and
 * fails with `Syntax error at offset N near chitin`. Anything that isn't
 * `[A-Za-z0-9_]` gets replaced with `_`. Multiple separators collapse to one.
 *
 * Examples:
 *   "chitin-sportsbook" → "chitin_sportsbook"
 *   "my.cool.project"   → "my_cool_project"
 *   "@scope/pkg"        → "scope_pkg"
 *   "indusk_already_ok" → "indusk_already_ok" (no change)
 */
export function sanitizeGroupId(raw) {
    return raw.replace(/[^A-Za-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
}
/**
 * Get the Graphiti group id for project-specific episodes.
 *
 * Resolution order:
 *   1. .indusk/config.json `graphiti.groupId` if set (used as-is, not sanitized —
 *      explicit overrides are trusted; if you set a hyphenated id, that's on you)
 *   2. Sanitized project directory basename (`-` → `_`, etc., for RediSearch safety)
 *
 * Use `[getProjectGroupId(root), "shared"]` as the default group_ids list when
 * searching Graphiti — this gives both project-scoped and cross-project knowledge.
 */
export function getProjectGroupId(projectRoot) {
    const config = readConfig(projectRoot);
    if (config?.graphiti?.groupId)
        return config.graphiti.groupId;
    return sanitizeGroupId(basename(projectRoot));
}
/**
 * Whether the OTel gate should fire for this project.
 *
 * Returns `true` if `.indusk/config.json` is missing, missing `otel.role`, or
 * has `otel.role: "service"`. Returns `false` only when the project explicitly
 * opts out via `otel.role: "library" | "tool" | "none"`.
 *
 * Used by:
 *   - planner skill (whether to write `#### Phase N OTel` sections into impl.md)
 *   - validate-impl-structure hook (whether to require an OTel section at write time)
 *   - check-gates hook (whether to block phase advancement on missing OTel)
 *
 * Backwards compatible: projects without the new field behave exactly as before.
 */
export function shouldEmitOtelGate(projectRoot) {
    const config = readConfig(projectRoot);
    const role = config?.otel?.role;
    return role === undefined || role === "service";
}
const DEFAULT_EVAL_MODEL = "sonnet";
/**
 * Model arg the eval agent passes to `claude --print --model <arg>` on fresh
 * first-call evals. Reads `eval.model` from `.indusk/config.json`; defaults to
 * `"sonnet"` when unset.
 *
 * Why default to sonnet: empirical pricing on resume calls (which don't pass
 * `--model` and inherit Claude Code's machine default) shows ~5× cheaper than
 * Opus. Defaulting fresh-call to Sonnet matches that behavior and saves the
 * catchup-cost spike. Set `eval.model: "opus"` to opt back into Opus.
 *
 * Returns the raw string — pass directly to `--model`. Accepts any value
 * Claude Code's `--model` accepts (`opus`, `sonnet`, `haiku`, full IDs).
 */
export function getEvalModel(projectRoot) {
    const config = readConfig(projectRoot);
    const model = config?.eval?.model;
    if (typeof model === "string" && model.length > 0)
        return model;
    return DEFAULT_EVAL_MODEL;
}
