//! File I/O service — public API for filesystem and shell operations.
//!
//! Exposes functions for reading, writing, and listing files scoped to the
//! active project root, plus utilities for absolute-path and shell operations.
//! HTTP handlers call these functions instead of touching `io::fs` directly.
//!
//! Conventions: `docs/architecture/service-modules.md`

pub(super) mod io;

use crate::state::SessionState;

/// Re-export the canonical filesystem entry type so HTTP handlers don't need
/// to import from `io::fs` directly.
pub use crate::io::fs::FileEntry;
/// Re-export the search result type.
pub use crate::io::search::SearchResult;
/// Re-export the shell output type.
pub use crate::io::shell::CommandOutput;

// ── Error type ────────────────────────────────────────────────────────────────

/// Typed errors returned by `service::file_io` functions.
///
/// HTTP handlers map these to status codes:
/// - [`Error::Validation`] → 400 Bad Request
/// - [`Error::Filesystem`] → 400 Bad Request (or 404 when appropriate)
#[derive(Debug)]
pub enum Error {
    /// The request was invalid (e.g. path traversal attempt, command not allowlisted).
    Validation(String),
    /// A filesystem or shell operation failed (file not found, permission denied, etc.).
    Filesystem(String),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Validation(msg) => write!(f, "Validation error: {msg}"),
            Self::Filesystem(msg) => write!(f, "Filesystem error: {msg}"),
        }
    }
}

// ── Path validation ───────────────────────────────────────────────────────────

/// Validate a relative path, rejecting directory traversal attempts.
///
/// Returns [`Error::Validation`] when the path contains `..`.
pub fn validate_path(path: &str) -> Result<(), Error> {
    if path.contains("..") {
        return Err(Error::Validation(
            "Security Violation: Directory traversal ('..') is not allowed.".to_string(),
        ));
    }
    Ok(())
}

// ── Public API ────────────────────────────────────────────────────────────────

/// Read a file from the project root.
pub async fn read_file(path: String, state: &SessionState) -> Result<String, Error> {
    validate_path(&path)?;
    io::read_file(path, state).await
}

/// Write a file to the project root, creating parent directories as needed.
#[allow(dead_code)]
pub async fn write_file(path: String, content: String, state: &SessionState) -> Result<(), Error> {
    validate_path(&path)?;
    io::write_file(path, content, state).await
}

/// List directory entries at a project-relative path.
#[allow(dead_code)]
pub async fn list_directory(path: String, state: &SessionState) -> Result<Vec<FileEntry>, Error> {
    io::list_directory(path, state).await
}

/// List directory entries at an absolute path (not scoped to the project root).
pub async fn list_directory_absolute(path: String) -> Result<Vec<FileEntry>, Error> {
    io::list_directory_absolute(path).await
}

/// Create a directory (and all parents) at an absolute path.
#[allow(dead_code)]
pub async fn create_directory_absolute(path: String) -> Result<(), Error> {
    io::create_directory_absolute(path).await
}

/// Return the current user's home directory path.
pub fn get_home_directory() -> Result<String, Error> {
    io::get_home_directory()
}

/// List all files in the project recursively, respecting `.gitignore`.
pub async fn list_project_files(state: &SessionState) -> Result<Vec<String>, Error> {
    io::list_project_files(state).await
}

/// Search the project for files whose contents contain `query`.
#[allow(dead_code)]
pub async fn search_files(query: String, state: &SessionState) -> Result<Vec<SearchResult>, Error> {
    io::search_files(query, state).await
}

/// Execute an allowlisted shell command in the project root directory.
#[allow(dead_code)]
pub async fn exec_shell(
    command: String,
    args: Vec<String>,
    state: &SessionState,
) -> Result<CommandOutput, Error> {
    io::exec_shell(command, args, state).await
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    // Pure unit tests for path validation and sanitisation — no tempdir, no network.

    #[test]
    fn validate_path_accepts_simple_relative_path() {
        assert!(validate_path("src/main.rs").is_ok());
    }

    #[test]
    fn validate_path_accepts_dot_path() {
        assert!(validate_path(".").is_ok());
    }

    #[test]
    fn validate_path_accepts_root_relative() {
        assert!(validate_path("subdir/file.txt").is_ok());
    }

    #[test]
    fn validate_path_rejects_parent_traversal() {
        let result = validate_path("../etc/passwd");
        assert!(matches!(result, Err(Error::Validation(_))));
    }

    #[test]
    fn validate_path_rejects_embedded_traversal() {
        let result = validate_path("src/../../../etc/passwd");
        assert!(matches!(result, Err(Error::Validation(_))));
    }

    #[test]
    fn validate_path_rejects_double_dot_only() {
        let result = validate_path("..");
        assert!(matches!(result, Err(Error::Validation(_))));
    }

    #[test]
    fn validate_path_accepts_file_with_single_dots_in_name() {
        // Filenames like "config.dev.toml" have single dots — must be accepted.
        assert!(validate_path("config.dev.toml").is_ok());
    }

    #[test]
    fn validate_path_rejects_traversal_with_url_encoding_lookalike() {
        // A literal ".." sequence anywhere in the string is rejected.
        let result = validate_path("valid/..hidden");
        assert!(matches!(result, Err(Error::Validation(_))));
    }

    #[test]
    fn error_display_validation() {
        let e = Error::Validation("bad path".to_string());
        assert!(e.to_string().contains("bad path"));
    }

    #[test]
    fn error_display_filesystem() {
        let e = Error::Filesystem("file not found".to_string());
        assert!(e.to_string().contains("file not found"));
    }

    #[test]
    fn error_display_filesystem_contains_message() {
        let e = Error::Filesystem("task panic".to_string());
        assert!(e.to_string().contains("task panic"));
    }
}
