use std::path::{Path, PathBuf};
use thiserror::Error;
use walkdir::WalkDir;

#[derive(Debug, Error)]
pub enum PathError {
    #[error("path traversal detected")]
    Traversal,
    #[error("file not found")]
    NotFound,
    #[error("only .puml files are served")]
    InvalidExtension,
}

/// Recursively list all .puml files under `root`, returning paths relative to `root`.
/// Separators are always `/` for consistent URL usage.
pub fn list_puml_files(root: &Path) -> Vec<String> {
    WalkDir::new(root)
        .follow_links(true)
        .max_open(100)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| {
            e.file_type().is_file() && e.path().extension().map(|x| x == "puml").unwrap_or(false)
        })
        .filter_map(|e| {
            e.path()
                .strip_prefix(root)
                .ok()
                .map(|rel| rel.to_string_lossy().replace('\\', "/"))
        })
        .collect()
}

/// Resolve a relative path against `root`, rejecting traversal and non-.puml files.
///
/// The returned path is canonical (symlinks resolved).
pub fn resolve_safe(root: &Path, rel: &str) -> Result<PathBuf, PathError> {
    // Quick string-level checks before touching the filesystem.
    if rel.contains("..") || rel.starts_with('/') || rel.starts_with('\\') {
        return Err(PathError::Traversal);
    }

    let candidate = root.join(rel);

    if candidate.extension().map(|x| x != "puml").unwrap_or(true) {
        return Err(PathError::InvalidExtension);
    }

    let canonical = candidate.canonicalize().map_err(|_| PathError::NotFound)?;
    let canonical_root = root.canonicalize().map_err(|_| PathError::NotFound)?;

    if !canonical.starts_with(&canonical_root) {
        return Err(PathError::Traversal);
    }

    Ok(canonical)
}

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

    fn make_tree() -> TempDir {
        let dir = TempDir::new().unwrap();
        fs::write(dir.path().join("a.puml"), "@startuml\n@enduml").unwrap();
        fs::create_dir(dir.path().join("sub")).unwrap();
        fs::write(dir.path().join("sub/b.puml"), "@startuml\n@enduml").unwrap();
        fs::write(dir.path().join("notes.txt"), "hello").unwrap();
        dir
    }

    #[test]
    fn lists_all_puml_files() {
        let dir = make_tree();
        let mut files = list_puml_files(dir.path());
        files.sort();
        assert_eq!(files.len(), 2);
        assert!(files.iter().any(|f| f == "a.puml"));
        assert!(files.iter().any(|f| f == "sub/b.puml"));
    }

    #[test]
    fn excludes_non_puml_files() {
        let dir = make_tree();
        let files = list_puml_files(dir.path());
        assert!(!files.iter().any(|f| f.ends_with(".txt")));
    }

    #[test]
    fn resolve_valid_file() {
        let dir = make_tree();
        assert!(resolve_safe(dir.path(), "a.puml").is_ok());
    }

    #[test]
    fn resolve_valid_nested_file() {
        let dir = make_tree();
        assert!(resolve_safe(dir.path(), "sub/b.puml").is_ok());
    }

    #[test]
    fn rejects_dotdot_traversal() {
        let dir = make_tree();
        assert!(matches!(
            resolve_safe(dir.path(), "../etc/passwd"),
            Err(PathError::Traversal)
        ));
    }

    #[test]
    fn rejects_absolute_path() {
        let dir = make_tree();
        assert!(matches!(
            resolve_safe(dir.path(), "/etc/passwd"),
            Err(PathError::Traversal)
        ));
    }

    #[test]
    fn rejects_wrong_extension() {
        let dir = make_tree();
        assert!(matches!(
            resolve_safe(dir.path(), "notes.txt"),
            Err(PathError::InvalidExtension)
        ));
    }

    #[test]
    fn rejects_nonexistent_file() {
        let dir = make_tree();
        assert!(matches!(
            resolve_safe(dir.path(), "ghost.puml"),
            Err(PathError::NotFound)
        ));
    }
}
