//! Volume root detection + v2 path helpers.
//!
//! `find_omne_root` walks up from a starting directory looking for an
//! ancestor that contains a `.omne/` subdirectory. `upgrade` and
//! `validate` use this so they work from any subdirectory of a volume;
//! `init` deliberately does **not** walk up (R13 — `init` creates in
//! the current directory only).
//!
//! The remaining helpers derive v2 volume paths from a root: static
//! `lib/{cfg,docs}` content, runtime `var/runs/<run_id>/{events.jsonl,
//! nodes/*}`, and per-run worktrees under `wt/<run_id>`. Centralizing
//! the layout here keeps the scaffold, runner, validator, and
//! event-log modules from drifting on directory names.
//!
//! The Python module's `is_mounted()` helper is **not ported** per R12:
//! the submodule/mount model was removed when releases-based
//! distribution replaced the submodule architecture.

#![allow(dead_code)]

use std::path::{Path, PathBuf};

/// `.omne/` root under the volume.
pub fn omne_dir(root: &Path) -> PathBuf {
    root.join(".omne")
}

/// `.omne/core/` — kernel tarball extraction target.
pub fn core_dir(root: &Path) -> PathBuf {
    omne_dir(root).join("core")
}

/// `.omne/dist/` — distro tarball extraction target (v1-renamed from
/// `image/`). Distro skill discovery and docs-baseline seeding both
/// key on this path.
pub fn dist_dir(root: &Path) -> PathBuf {
    omne_dir(root).join("dist")
}

/// `.omne/lib/cfg/` — per-volume static configuration.
pub fn cfg_dir(root: &Path) -> PathBuf {
    omne_dir(root).join("lib").join("cfg")
}

/// `.omne/lib/docs/` — knowledge-base root.
pub fn docs_dir(root: &Path) -> PathBuf {
    omne_dir(root).join("lib").join("docs")
}

/// `.omne/lib/docs/index.md` — the docs baseline entry point.
pub fn docs_baseline(root: &Path) -> PathBuf {
    docs_dir(root).join("index.md")
}

/// `.omne/var/` — runtime state root. Contains `runs/` and the
/// monotonic-ULID lock file.
pub fn var_dir(root: &Path) -> PathBuf {
    omne_dir(root).join("var")
}

/// `.omne/var/.ulid-last` — advisory-locked file holding the last
/// allocated ULID for cross-process monotonicity.
pub fn ulid_lock_path(root: &Path) -> PathBuf {
    var_dir(root).join(".ulid-last")
}

/// `.omne/var/runs/` — per-run directories live under this.
pub fn runs_dir(root: &Path) -> PathBuf {
    var_dir(root).join("runs")
}

/// `.omne/var/runs/<run_id>/` — a specific run's state directory.
pub fn run_dir(root: &Path, run_id: &str) -> PathBuf {
    runs_dir(root).join(run_id)
}

/// `.omne/var/runs/<run_id>/events.jsonl` — the per-run event log.
pub fn events_log_path(root: &Path, run_id: &str) -> PathBuf {
    run_dir(root, run_id).join("events.jsonl")
}

/// `.omne/var/runs/<run_id>/nodes/` — per-node capture directory.
pub fn nodes_dir(root: &Path, run_id: &str) -> PathBuf {
    run_dir(root, run_id).join("nodes")
}

/// Forward-slash, volume-root-relative wire path for a node's capture
/// file. Always `.omne/var/runs/<run_id>/nodes/<node_id>.out` with
/// forward slashes regardless of host OS — written into the
/// `output_path` field of `node.completed` events so Windows and Unix
/// readers see the same wire shape.
pub fn node_capture_wire_path(run_id: &str, node_id: &str) -> String {
    format!(".omne/var/runs/{run_id}/nodes/{node_id}.out")
}

/// `.omne/wt/` — worktree root.
pub fn wt_dir(root: &Path) -> PathBuf {
    omne_dir(root).join("wt")
}

/// `.omne/wt/<run_id>/` — the detached worktree for a specific run.
pub fn wt_for(root: &Path, run_id: &str) -> PathBuf {
    wt_dir(root).join(run_id)
}

/// Walk up from `start` looking for an ancestor that contains a
/// `.omne/` subdirectory. Returns the first match, or `None` when the
/// walk reaches the filesystem root without finding one.
///
/// `start` is canonicalized first so relative paths, `..` segments,
/// and symlinks are resolved before the walk begins. If `start` does
/// not exist or cannot be canonicalized, the function returns `None`
/// — the caller surfaces this as `CliError::NotAVolume` at the
/// command boundary.
pub fn find_omne_root(start: &Path) -> Option<PathBuf> {
    let mut current = start.canonicalize().ok()?;
    loop {
        if current.join(".omne").is_dir() {
            return Some(current);
        }
        if !current.pop() {
            return None;
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn finds_root_when_omne_exists_at_start() {
        let tmp = TempDir::new().expect("create tempdir");
        fs::create_dir(tmp.path().join(".omne")).expect("create .omne");

        let found = find_omne_root(tmp.path());
        let expected = tmp.path().canonicalize().expect("canonicalize tmpdir");
        assert_eq!(found, Some(expected));
    }

    #[test]
    fn finds_root_from_subdirectory() {
        let tmp = TempDir::new().expect("create tempdir");
        fs::create_dir(tmp.path().join(".omne")).expect("create .omne");

        let deep = tmp.path().join("src").join("deep");
        fs::create_dir_all(&deep).expect("create deep subdir");

        let found = find_omne_root(&deep);
        let expected = tmp.path().canonicalize().expect("canonicalize tmpdir");
        assert_eq!(found, Some(expected));
    }

    #[test]
    fn walk_up_matches_actual_ancestor_chain() {
        // Do NOT create `.omne` in the tempdir itself. Derive the
        // expected result from the canonicalized ancestor chain so the
        // test stays hermetic even if some ambient ancestor (e.g.
        // `/tmp/.omne` on a developer machine) already contains a
        // `.omne` directory. In a clean environment this still asserts
        // `None`; on a contaminated environment it asserts the
        // contaminating ancestor's path instead of failing.
        let tmp = TempDir::new().expect("create tempdir");
        let canonical_start = tmp.path().canonicalize().expect("canonicalize tmpdir");
        let expected = canonical_start
            .ancestors()
            .find(|ancestor| ancestor.join(".omne").is_dir())
            .map(Path::to_path_buf);

        let found = find_omne_root(tmp.path());
        assert_eq!(found, expected);
    }

    #[test]
    fn returns_none_when_start_does_not_exist() {
        let tmp = TempDir::new().expect("create tempdir");
        let missing = tmp.path().join("does-not-exist");

        let found = find_omne_root(&missing);
        assert_eq!(found, None);
    }
}
