/// On-disk state for ritalin contracts.
///
/// Layout (relative to current working directory):
///   .ritalin/scope.yaml         — human-edited contract (outcome + metadata)
///   .ritalin/obligations.jsonl  — append-only obligation ledger
///   .ritalin/evidence.jsonl     — append-only verification evidence ledger
///   .task-incomplete            — marker file; presence = "agent must keep working"
///
/// Why JSONL for ledgers? Append-only single-syscall writes are line-atomic
/// on POSIX, so concurrent writers never corrupt the ledger. Operations that
/// need read-then-write (id allocation, gate's pass-commit, --force rewrites)
/// serialize through the `.ritalin/.lock` contract lock (see `lock_state`).
///
/// Why YAML for scope? Humans (and agents) read and write it directly;
/// JSON's lack of comments makes it hostile to in-line acceptance criteria.
pub mod evidence;
pub mod marker;
pub mod obligations;
pub mod scope;
pub mod workspace_hash;

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

use crate::error::AppError;

/// Walk up from `cwd` looking for a `.ritalin/` directory, without crossing
/// into a *different* project's territory. A directory containing a `.git`
/// directory but no `.ritalin` is an independent repo root — the walk stops
/// there, so a stray `.ritalin` in e.g. `~/Projects` cannot capture the Stop
/// hook of every repo nested beneath it. A `.git` *file* (submodule or
/// linked worktree) continues the walk only when its owning repository is
/// itself an ancestor — always true for submodules, but a linked worktree
/// can be parked anywhere, and one nested under a foreign directory must
/// not adopt that directory's contract. Outside any git repo the walk
/// continues to the filesystem root (pre-v0.4.5 behavior).
fn find_state_dir(cwd: &Path) -> Option<PathBuf> {
    let mut current = Some(cwd);
    while let Some(dir) = current {
        let candidate = dir.join(".ritalin");
        if candidate.exists() {
            return Some(candidate);
        }
        let git = dir.join(".git");
        if git.is_dir() || (git.is_file() && !owned_by_ancestor(dir)) {
            return None;
        }
        current = dir.parent();
    }
    None
}

/// For a directory whose `.git` is a file (submodule or linked worktree),
/// decide whether the repository that owns it sits at an ancestor of `dir`.
/// The file's `gitdir:` target always lives inside the owner's `.git`
/// directory (`<owner>/.git/modules/…` for submodules, `<owner>/.git/
/// worktrees/…` for linked worktrees), so the owner root is the path prefix
/// ending at the `.git` component. Submodules structurally satisfy this;
/// worktrees only when kept inside their main checkout. Anything
/// unparseable or unresolvable is treated as NOT subordinate — the walk
/// stops, which fails toward containment rather than contract capture.
fn owned_by_ancestor(dir: &Path) -> bool {
    let Ok(raw) = std::fs::read_to_string(dir.join(".git")) else {
        return false;
    };
    let Some(target) = raw.trim().strip_prefix("gitdir:") else {
        return false;
    };
    let target = target.trim();
    let resolved = if Path::new(target).is_absolute() {
        PathBuf::from(target)
    } else {
        dir.join(target)
    };
    let (Ok(resolved), Ok(dir_canon)) =
        (std::fs::canonicalize(&resolved), std::fs::canonicalize(dir))
    else {
        return false;
    };
    let mut current = resolved.as_path();
    while let Some(parent) = current.parent() {
        if current.file_name().is_some_and(|n| n == ".git") {
            return dir_canon.starts_with(parent);
        }
        current = parent;
    }
    // No `.git` component (e.g. --separate-git-dir): an independent repo.
    false
}

/// Acquire the exclusive contract lock (`.ritalin/.lock`), serializing id
/// allocation (`add`), destructive rewrites (`init`/`seed --force`), and
/// gate's pass-commit against each other. The lock lives on a dedicated
/// file that is never deleted — locking the ledger itself would guard
/// nothing once `--force` unlinks and recreates it, and append-only handles
/// cannot be locked on Windows. Advisory; released when the returned handle
/// drops (including on crash).
pub fn lock_state(state_dir: &Path) -> Result<std::fs::File, AppError> {
    std::fs::create_dir_all(state_dir)?;
    let file = std::fs::OpenOptions::new()
        .create(true)
        .truncate(false)
        .write(true)
        .open(state_dir.join(".lock"))?;
    file.lock()?;
    Ok(file)
}

/// Find the ritalin state directory by walking up from cwd (stopping at the
/// git-repo boundary). Returns `.ritalin/` next to the first ancestor that
/// contains it, or `<cwd>/.ritalin/` if none is found.
pub fn state_dir(cwd: &Path) -> PathBuf {
    find_state_dir(cwd).unwrap_or_else(|| cwd.join(".ritalin"))
}

/// Returns true if `.ritalin/` exists in the cwd ancestry within the current
/// git repo (or anywhere up the tree when not in a git repo).
pub fn is_initialized(cwd: &Path) -> bool {
    find_state_dir(cwd).is_some()
}
