//! # motte
//!
//! Defensive mount and root workspace helper for terminal environments.
//!
//! `motte` (from Old English *mot*, a mound or defensive hillock) provides
//! workspace root detection, mount point resolution, and path ancestry
//! walking for building secure terminal environments.
//!
//! ## Example
//!
//! ```
//! use motte::{WorkspaceRoot, MountPoint, PathAncestry};
//!
//! // Detect workspace root by walking up the tree
//! let root_finder = WorkspaceRoot::default();
//! let root = root_finder.find_from(std::env::current_dir().unwrap());
//! assert!(root.is_some());
//!
//! // Check if a path is a mount point
//! let mp = MountPoint::new("/");
//! assert!(mp.is_root());
//!
//! // Walk path ancestry
//! let ancestry = PathAncestry::new("/a/b/c/d");
//! assert_eq!(ancestry.depth(), 5);
//! assert_eq!(ancestry.ancestor(1), std::path::Path::new("/a/b/c"));
//! ```

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

/// Markers for finding workspace roots.
///
/// A workspace root is identified by the presence of marker files
/// or directories (like `.git`, `Cargo.toml`, etc.).
///
/// # Examples
///
/// ```
/// use motte::WorkspaceRoot;
///
/// let markers = WorkspaceRoot::with_markers(&["Cargo.toml", ".git"]);
/// // This checks the current file's directory upward
/// ```
#[derive(Debug, Clone)]
pub struct WorkspaceRoot {
    markers: Vec<String>,
}

impl WorkspaceRoot {
    /// Create a root finder with the given marker names.
    pub fn with_markers(markers: &[&str]) -> Self {
        Self {
            markers: markers.iter().map(|s| s.to_string()).collect(),
        }
    }

    /// Create a root finder with common Rust/project markers.
    pub fn rust_defaults() -> Self {
        Self::with_markers(&["Cargo.toml", "Cargo.lock", ".git"])
    }

    /// Search upward from the given path for a workspace root.
    pub fn find_from(&self, start: impl AsRef<Path>) -> Option<PathBuf> {
        let mut current = start.as_ref().to_path_buf();

        loop {
            // Check if any marker exists in the current directory
            for marker in &self.markers {
                let candidate = current.join(marker);
                if candidate.exists() {
                    return Some(current);
                }
            }

            // Move up
            if !current.pop() {
                break;
            }
        }

        None
    }

    /// Get the list of markers being searched for.
    pub fn markers(&self) -> &[String] {
        &self.markers
    }
}

impl Default for WorkspaceRoot {
    fn default() -> Self {
        Self::rust_defaults()
    }
}

/// Represents a filesystem mount point.
///
/// Mount points are used to identify boundaries between different
/// filesystems or security domains.
///
/// # Examples
///
/// ```
/// use motte::MountPoint;
///
/// let mp = MountPoint::new("/");
/// assert!(mp.is_root());
///
/// let mp = MountPoint::new("/home");
/// assert!(!mp.is_root());
/// assert_eq!(mp.path(), std::path::Path::new("/home"));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountPoint {
    path: PathBuf,
}

impl MountPoint {
    /// Create a mount point from a path.
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self { path: path.into() }
    }

    /// Returns true if this is the filesystem root (`/`).
    pub fn is_root(&self) -> bool {
        self.path == Path::new("/")
    }

    /// Get the mount point path.
    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Check if the given path is under this mount point.
    pub fn contains(&self, path: impl AsRef<Path>) -> bool {
        path.as_ref().starts_with(&self.path)
    }

    /// Get the parent mount point (one level up).
    pub fn parent(&self) -> Option<MountPoint> {
        self.path.parent().map(|p| MountPoint::new(p.to_path_buf()))
    }

    /// List direct children of this mount point (if it exists on disk).
    pub fn children(&self) -> Result<Vec<PathBuf>, std::io::Error> {
        let mut children = Vec::new();
        for entry in std::fs::read_dir(&self.path)? {
            let entry = entry?;
            children.push(entry.path());
        }
        children.sort();
        Ok(children)
    }
}

/// Walk the ancestry of a path from leaf to root.
///
/// # Examples
///
/// ```
/// use motte::PathAncestry;
///
/// let ancestry = PathAncestry::new("/a/b/c/d");
/// assert_eq!(ancestry.depth(), 5);
/// assert_eq!(ancestry.leaf(), std::path::Path::new("d"));
/// assert_eq!(ancestry.ancestor(1), std::path::Path::new("/a/b/c"));
/// ```
#[derive(Debug, Clone)]
pub struct PathAncestry {
    components: Vec<PathBuf>,
}

impl PathAncestry {
    /// Create a new ancestry walker for the given path.
    pub fn new(path: impl AsRef<Path>) -> Self {
        let mut components = Vec::new();
        let mut current = Some(path.as_ref().to_path_buf());

        while let Some(p) = current {
            components.push(p.clone());
            current = p.parent().map(|pp| pp.to_path_buf());
        }

        Self { components }
    }

    /// The number of ancestor entries including root.
    pub fn depth(&self) -> usize {
        self.components.len()
    }

    /// The leaf (basename) of the path.
    pub fn leaf(&self) -> &Path {
        if self.components.is_empty() {
            Path::new("")
        } else {
            self.components[0]
                .file_name()
                .map(|n| Path::new(n))
                .unwrap_or(Path::new(""))
        }
    }

    /// Get the ancestor at the given depth (0 = leaf, depth-1 = root).
    pub fn ancestor(&self, depth: usize) -> &Path {
        if depth >= self.components.len() {
            Path::new("")
        } else {
            &self.components[depth]
        }
    }

    /// Check if the path is absolute.
    pub fn is_absolute(&self) -> bool {
        self.components
            .last()
            .map(|p| p.is_absolute())
            .unwrap_or(false)
    }

    /// Get the root component of the path.
    pub fn root(&self) -> &Path {
        self.components
            .last()
            .map(|p| p.as_path())
            .unwrap_or(Path::new(""))
    }
}

/// A defensive workspace boundary that validates all path operations.
///
/// Combines workspace root detection, mount point awareness, and ancestry
/// walking into a single safety-checked interface.
///
/// # Examples
///
/// ```
/// use motte::DefensiveBoundary;
///
/// let boundary = DefensiveBoundary::new("/tmp/project");
/// let resolved = boundary.resolve_safe("src/main.rs").unwrap();
/// assert!(resolved.starts_with("/tmp/project"));
/// ```
#[derive(Debug, Clone)]
pub struct DefensiveBoundary {
    root: PathBuf,
}

impl DefensiveBoundary {
    /// Create a new defensive boundary at the given root.
    pub fn new(root: impl Into<PathBuf>) -> Self {
        Self { root: root.into() }
    }

    /// Get the boundary root.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// Check if a path is within the boundary.
    pub fn contains(&self, path: impl AsRef<Path>) -> bool {
        let path = path.as_ref();
        if path.is_absolute() {
            path.starts_with(&self.root)
        } else {
            // Relative paths are always considered inside
            true
        }
    }

    /// Safely resolve a relative path against the boundary root.
    pub fn resolve_safe(&self, relative: impl AsRef<Path>) -> Result<PathBuf, BoundaryError> {
        let relative = relative.as_ref();
        let candidate = self.root.join(relative);

        let canonical = self.canonicalize_or_clean(&candidate);

        if !canonical.starts_with(&self.root) {
            return Err(BoundaryError::Escape {
                root: self.root.display().to_string(),
                path: relative.display().to_string(),
            });
        }

        Ok(canonical)
    }

    /// Walk the ancestry of a path within the boundary.
    pub fn ancestry(&self, relative: impl AsRef<Path>) -> Result<PathAncestry, BoundaryError> {
        let resolved = self.resolve_safe(relative)?;
        Ok(PathAncestry::new(resolved))
    }

    fn canonicalize_or_clean(&self, path: &Path) -> PathBuf {
        if let Ok(c) = path.canonicalize() {
            return c;
        }
        let mut components = Vec::new();
        for comp in path.components() {
            match comp {
                std::path::Component::ParentDir => {
                    components.pop();
                }
                std::path::Component::CurDir => {}
                other => components.push(other),
            }
        }
        components.iter().collect()
    }
}

/// Errors from defensive boundary operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BoundaryError {
    /// Path escapes the boundary root.
    Escape { root: String, path: String },
}

impl std::fmt::Display for BoundaryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            BoundaryError::Escape { root, path } => {
                write!(f, "path '{}' escapes boundary root '{}'", path, root)
            }
        }
    }
}

impl std::error::Error for BoundaryError {}

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

    fn temp_dir(name: &str) -> PathBuf {
        let dir = std::env::temp_dir().join(format!("motte_test_{}", name));
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn workspace_root_default_markers() {
        let wr = WorkspaceRoot::default();
        assert!(wr.markers().contains(&"Cargo.toml".to_string()));
    }

    #[test]
    fn workspace_root_custom_markers() {
        let wr = WorkspaceRoot::with_markers(&["my-marker"]);
        assert_eq!(wr.markers(), &["my-marker"]);
    }

    #[test]
    fn mount_point_root() {
        let mp = MountPoint::new("/");
        assert!(mp.is_root());
    }

    #[test]
    fn mount_point_not_root() {
        let mp = MountPoint::new("/home");
        assert!(!mp.is_root());
    }

    #[test]
    fn mount_point_contains() {
        let mp = MountPoint::new("/tmp");
        assert!(mp.contains("/tmp/file.txt"));
        assert!(!mp.contains("/etc/passwd"));
    }

    #[test]
    fn mount_point_parent() {
        let mp = MountPoint::new("/tmp/sandbox");
        let parent = mp.parent().unwrap();
        assert_eq!(parent.path(), Path::new("/tmp"));
    }

    #[test]
    fn mount_point_children() {
        let dir = temp_dir("mount_children");
        fs::write(dir.join("a.txt"), "a").unwrap();
        fs::write(dir.join("b.txt"), "b").unwrap();

        let mp = MountPoint::new(&dir);
        let children = mp.children().unwrap();
        assert_eq!(children.len(), 2);
    }

    #[test]
    fn path_ancestry_depth() {
        let a = PathAncestry::new("/a/b/c/d");
        assert_eq!(a.depth(), 5);
    }

    #[test]
    fn path_ancestry_leaf() {
        let a = PathAncestry::new("/a/b/c/d");
        assert_eq!(a.leaf(), Path::new("d"));
    }

    #[test]
    fn path_ancestry_ancestor() {
        let a = PathAncestry::new("/a/b/c/d");
        assert_eq!(a.ancestor(1), Path::new("/a/b/c"));
        assert_eq!(a.ancestor(3), Path::new("/a"));
    }

    #[test]
    fn path_ancestry_absolute() {
        let a = PathAncestry::new("/abs/path");
        assert!(a.is_absolute());

        let a = PathAncestry::new("rel/path");
        assert!(!a.is_absolute());
    }

    #[test]
    fn defensive_boundary_contains() {
        let b = DefensiveBoundary::new("/tmp/project");
        assert!(b.contains("/tmp/project/src/main.rs"));
        assert!(!b.contains("/etc/passwd"));
        assert!(b.contains("relative/path"));
    }

    #[test]
    fn defensive_boundary_resolve_safe() {
        let b = DefensiveBoundary::new("/tmp/project");
        let resolved = b.resolve_safe("src/main.rs").unwrap();
        assert!(resolved.starts_with("/tmp/project"));
    }

    #[test]
    fn defensive_boundary_escape() {
        let b = DefensiveBoundary::new("/tmp/project");
        let result = b.resolve_safe("../../etc/passwd");
        assert!(result.is_err());
    }

    #[test]
    fn defensive_boundary_ancestry() {
        let b = DefensiveBoundary::new("/tmp/project");
        let a = b.ancestry("src/main.rs").unwrap();
        assert!(a.depth() > 0);
    }

    #[test]
    fn boundary_error_display() {
        let err = BoundaryError::Escape {
            root: "/tmp".into(),
            path: "../etc/passwd".into(),
        };
        assert!(err.to_string().contains("etc/passwd"));
    }
}
