//! File read operation with sandboxing and line/limit support.
//!
//! **Status**: Implemented (TOOLS-FS-01)
//!
//! This module implements:
//! - Path validation using the security layer
//! - Asynchronous file reading
//! - UTF-8 encoding validation
//! - Line range and limit semantics
//! - Sandbox containment checks
//! - Blocklist enforcement
//! - Size limit soft caps

use crate::config::SandboxConfig;
use crate::error::{ToolError, ToolResult};
use crate::path::validate_path;
use serde::{Deserialize, Serialize};

/// Request to read a text file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadTextFileRequest {
    /// Absolute path to the file to read.
    pub path: String,

    /// Optional starting line (1-indexed).
    ///
    /// If provided with limit, reads from this line.
    /// If provided without limit, reads from this line to end.
    pub line: Option<usize>,

    /// Optional maximum number of lines to read.
    ///
    /// If provided without line, reads first N lines.
    /// If provided with line, reads N lines starting from line.
    pub limit: Option<usize>,
}

/// Response from reading a text file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadTextFileResponse {
    /// UTF-8 text content of the file (or portion thereof).
    pub content: String,
}

/// Read a text file with sandboxing and line/limit support.
///
/// ## Line/Limit Semantics
///
/// - `line: None, limit: None` → Read entire file
/// - `line: Some(n), limit: None` → Read from line n to end
/// - `line: None, limit: Some(m)` → Read first m lines
/// - `line: Some(n), limit: Some(m)` → Read m lines starting from line n
///
/// ## Error Cases
///
/// - Read disabled → `ToolError::PermissionDenied`
/// - Path outside allowed roots → `ToolError::SandboxViolation`
/// - Path matches blocklist → `ToolError::BlockedPath`
/// - File not found → `ToolError::NotFound`
/// - Non-UTF-8 content → `ToolError::EncodingUnsupported`
/// - File too large (soft cap) → Warning logged, content returned
///
/// ## See Also
///
/// - Task spec: `docs/building/04_acp_client/04_tasks_02_tools_and_sandboxing.md#TOOLS-FS-01`
/// - Security spec: `docs/building/04_acp_client/03_fs_sandboxing_and_permissions_spec.md`
pub async fn read_text_file(
    request: ReadTextFileRequest,
    config: &SandboxConfig,
) -> ToolResult<ReadTextFileResponse> {
    // Check if read is enabled
    if !config.read_enabled {
        return Err(ToolError::permission_denied("Read operations are disabled"));
    }

    // Validate and canonicalize path (checks containment and blocklist)
    let canonical_path = validate_path(&request.path, config)?;

    // Read file asynchronously
    let content_bytes = tokio::fs::read(&canonical_path)
        .await
        .map_err(|e| {
            if e.kind() == std::io::ErrorKind::NotFound {
                ToolError::NotFound {
                    path: request.path.clone(),
                }
            } else {
                ToolError::FileReadError {
                    path: request.path.clone(),
                    source: e,
                }
            }
        })?;

    // Check soft cap and log warning if exceeded
    let file_size = content_bytes.len() as u64;
    if file_size > config.max_read_bytes {
        tracing::warn!(
            path = %request.path,
            size = file_size,
            limit = config.max_read_bytes,
            "File size exceeds max_read_bytes soft cap"
        );
    }

    // Validate UTF-8 encoding
    let content = String::from_utf8(content_bytes).map_err(|_| {
        ToolError::EncodingUnsupported {
            encoding: "non-UTF-8".to_string(),
        }
    })?;

    // Apply line/limit semantics
    let final_content = apply_line_limit(&content, request.line, request.limit);

    Ok(ReadTextFileResponse {
        content: final_content,
    })
}

/// Apply line/limit semantics to file content.
///
/// ## Semantics
///
/// - `line: None, limit: None` → Return entire content
/// - `line: Some(n), limit: None` → Return from line n to end (1-indexed)
/// - `line: None, limit: Some(m)` → Return first m lines
/// - `line: Some(n), limit: Some(m)` → Return m lines starting from line n (1-indexed)
fn apply_line_limit(content: &str, line: Option<usize>, limit: Option<usize>) -> String {
    match (line, limit) {
        // No line or limit specified - return entire content
        (None, None) => content.to_string(),

        // Only limit specified - return first N lines
        (None, Some(limit)) => {
            content
                .lines()
                .take(limit)
                .collect::<Vec<_>>()
                .join("\n")
        }

        // Only line specified - return from line N to end (1-indexed)
        (Some(start_line), None) => {
            if start_line == 0 {
                return content.to_string();
            }
            content
                .lines()
                .skip(start_line.saturating_sub(1))
                .collect::<Vec<_>>()
                .join("\n")
        }

        // Both line and limit specified - return N lines starting from line M (1-indexed)
        (Some(start_line), Some(limit)) => {
            if start_line == 0 {
                return content
                    .lines()
                    .take(limit)
                    .collect::<Vec<_>>()
                    .join("\n");
            }
            content
                .lines()
                .skip(start_line.saturating_sub(1))
                .take(limit)
                .collect::<Vec<_>>()
                .join("\n")
        }
    }
}

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

    #[test]
    fn test_apply_line_limit_no_params() {
        let content = "line1\nline2\nline3";
        assert_eq!(apply_line_limit(content, None, None), content);
    }

    #[test]
    fn test_apply_line_limit_only_limit() {
        let content = "line1\nline2\nline3\nline4";
        assert_eq!(apply_line_limit(content, None, Some(2)), "line1\nline2");
    }

    #[test]
    fn test_apply_line_limit_only_line() {
        let content = "line1\nline2\nline3\nline4";
        assert_eq!(apply_line_limit(content, Some(2), None), "line2\nline3\nline4");
    }

    #[test]
    fn test_apply_line_limit_both() {
        let content = "line1\nline2\nline3\nline4\nline5";
        assert_eq!(apply_line_limit(content, Some(2), Some(2)), "line2\nline3");
    }

    #[test]
    fn test_apply_line_limit_start_zero() {
        let content = "line1\nline2\nline3";
        assert_eq!(apply_line_limit(content, Some(0), None), content);
        assert_eq!(apply_line_limit(content, Some(0), Some(2)), "line1\nline2");
    }

    #[test]
    fn test_apply_line_limit_beyond_end() {
        let content = "line1\nline2\nline3";
        assert_eq!(apply_line_limit(content, Some(10), None), "");
        assert_eq!(apply_line_limit(content, Some(2), Some(10)), "line2\nline3");
    }
}
