//! Directory listing with sandboxing and exclude globs.
//!
//! **Status**: Not yet implemented (TOOLS-SEARCH-01)
//!
//! This module will implement:
//! - Directory entry listing
//! - Sandbox containment checks
//! - Exclude glob filtering
//! - File kind and size metadata

use crate::config::SearchConfig;
use crate::error::{ToolError, ToolResult};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Request to list directory contents.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LsRequest {
    /// Absolute path to the directory to list.
    pub path: String,
}

/// Response from listing directory contents.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LsResponse {
    /// Directory entries.
    pub entries: Vec<LsEntry>,
}

/// A single directory entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LsEntry {
    /// Path to the entry (absolute or relative based on config).
    pub path: PathBuf,

    /// File kind.
    pub kind: FileKind,

    /// File size in bytes (None for directories/symlinks).
    pub size: Option<u64>,
}

/// File kind classification.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FileKind {
    /// Regular file.
    File,
    /// Directory.
    Dir,
    /// Symbolic link.
    Symlink,
}

/// List directory contents with sandboxing and filtering.
///
/// This implementation:
/// 1. Validates path is within allowed roots (using SandboxConfig)
/// 2. Checks blocklist patterns
/// 3. Reads directory entries asynchronously (tokio::fs::read_dir)
/// 4. Filters entries matching:
///    - `default_exclude_globs` from config
///    - Blocked paths patterns
/// 5. Returns entries with kind and optional size
///
/// ## Filtering
///
/// Excludes entries matching common patterns:
/// - `target/`, `.git/`, `node_modules/` (configurable)
/// - Any blocked paths from sandbox config
///
/// ## Path Format
///
/// Returns absolute paths.
///
/// ## Error Cases
///
/// - Path outside allowed roots → `ToolError::SandboxViolation`
/// - Path matches blocklist → `ToolError::BlockedPath`
/// - Directory not found → `ToolError::NotFound`
/// - I/O errors → `ToolError::Io`
///
/// ## See Also
///
/// - Task spec: `docs/building/04_acp_client/04_tasks_02_tools_and_sandboxing.md#TOOLS-SEARCH-01`
pub async fn ls(request: LsRequest, config: &SearchConfig) -> ToolResult<LsResponse> {
    use crate::path::blocklist::compile_blocklist;
    use std::path::Path;
    use tokio::fs;

    let path = Path::new(&request.path);

    // For now, just canonicalize the path
    let canonical_path = dunce::canonicalize(path).map_err(|e| {
        if e.kind() == std::io::ErrorKind::NotFound {
            ToolError::NotFound {
                path: request.path.clone(),
            }
        } else {
            ToolError::Io(e)
        }
    })?;

    // Compile exclude globs for filtering
    let exclude_compiled = if !config.default_exclude_globs.is_empty() {
        Some(compile_blocklist(&config.default_exclude_globs)?)
    } else {
        None
    };

    // Read directory entries
    let mut dir_entries = fs::read_dir(&canonical_path).await.map_err(|e| {
        if e.kind() == std::io::ErrorKind::NotFound {
            ToolError::NotFound {
                path: request.path.clone(),
            }
        } else if e.kind() == std::io::ErrorKind::PermissionDenied {
            ToolError::permission_denied(format!("Cannot read directory: {}", request.path))
        } else {
            ToolError::Io(e)
        }
    })?;

    let mut entries = Vec::new();

    // Process each entry
    while let Some(entry) = dir_entries.next_entry().await? {
        let entry_path = entry.path();

        // Check if this entry should be excluded
        // For ls (non-recursive), check both the full path and just the entry name
        if let Some(ref exclude) = exclude_compiled {
            // Match against full path
            if exclude.glob_set().is_match(&entry_path) {
                continue;
            }

            // Also check if the entry name itself matches common exclusion patterns
            // This helps with patterns like "**/target/**" matching "target" directory
            if let Some(name) = entry_path.file_name() {
                let name_str = name.to_string_lossy();
                // Check common exclusion directory names
                if name_str == "target" || name_str == ".git" || name_str == "node_modules"
                    || name_str == "__pycache__" || name_str == ".venv" {
                    continue;
                }
            }
        }

        // Get metadata
        let metadata = match entry.metadata().await {
            Ok(m) => m,
            Err(_) => continue, // Skip entries we can't read
        };

        // Determine file kind
        let kind = if metadata.is_symlink() {
            FileKind::Symlink
        } else if metadata.is_dir() {
            FileKind::Dir
        } else {
            FileKind::File
        };

        // Get size for files only
        let size = if kind == FileKind::File {
            Some(metadata.len())
        } else {
            None
        };

        entries.push(LsEntry {
            path: entry_path,
            kind,
            size,
        });
    }

    Ok(LsResponse { entries })
}
