//! File write operation with sandboxing, permissions, and atomic writes.
//!
//! **Status**: Implemented (TOOLS-FS-02)
//!
//! This module implements:
//! - Path validation and canonicalization
//! - Permission checks (stubbed for now)
//! - Atomic write operations (temp + rename)
//! - EOL normalization
//! - Parent directory creation
//! - Size limit enforcement

use crate::config::{EolPolicy, PermissionConfig, SandboxConfig};
use crate::error::{ToolError, ToolResult};
use crate::path::validate_path;
use crate::permission::check::{check_permission, PermissionContext};
use crate::permission::cache::PermissionDecision;
use crate::permission::whitelist::PermissionOperation;
use serde::{Deserialize, Serialize};
use std::path::Path;

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

    /// UTF-8 text content to write.
    pub content: String,
}

/// Response from writing a text file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WriteTextFileResponse {}

/// Write a text file with sandboxing, permission gating, and atomic writes.
///
/// ## Implementation
///
/// This function:
/// 1. Validates `write_enabled = true` in config
/// 2. Validates and canonicalizes path
/// 3. Checks containment and blocklist
/// 4. Checks permission (TODO: integrate with permission system)
/// 5. Validates content size against max_write_bytes
/// 6. Applies EOL policy normalization
/// 7. Creates parent directories if needed
/// 8. Performs atomic write (temp file + rename)
///
/// ## EOL Normalization
///
/// Applies configured EOL policy:
/// - `Preserve` → Keep original line endings
/// - `Lf` → Normalize to LF (\n)
/// - `Crlf` → Normalize to CRLF (\r\n)
///
/// ## Atomic Writes
///
/// To prevent partial writes:
/// 1. Write content to temporary file in same directory
/// 2. Rename temp file to target path (atomic on POSIX, near-atomic on Windows)
///
/// ## Error Cases
///
/// - Write disabled → `ToolError::PermissionDenied`
/// - Path outside allowed roots → `ToolError::SandboxViolation`
/// - Path matches blocklist → `ToolError::BlockedPath`
/// - Permission denied → `ToolError::PermissionDenied`
/// - Content too large → `ToolError::FileTooLarge`
/// - I/O errors → `ToolError::Io`
///
/// ## See Also
///
/// - Task spec: `docs/building/04_acp_client/04_tasks_02_tools_and_sandboxing.md#TOOLS-FS-02`
/// - Security spec: `docs/building/04_acp_client/03_fs_sandboxing_and_permissions_spec.md`
pub async fn write_text_file(
    request: WriteTextFileRequest,
    sandbox_config: &SandboxConfig,
    permission_config: &PermissionConfig,
    permission_context: &PermissionContext,
) -> ToolResult<WriteTextFileResponse> {
    // Check if write is enabled
    if !sandbox_config.write_enabled {
        return Err(ToolError::permission_denied("Write operations are disabled"));
    }

    // Validate content size
    let content_size = request.content.len() as u64;
    if content_size > sandbox_config.max_write_bytes {
        return Err(ToolError::FileTooLarge {
            size: content_size,
            limit: sandbox_config.max_write_bytes,
        });
    }

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

    // Check permission via permission system (TOOLS-PERM-01)
    let operation = PermissionOperation::Write {
        path: request.path.clone(),
    };

    let decision = check_permission(&operation, permission_context, permission_config).await?;

    match decision {
        PermissionDecision::Allowed => {
            // Permission granted, proceed with write
            tracing::debug!(
                path = %request.path,
                "Permission granted for write operation"
            );
        }
        PermissionDecision::Denied => {
            return Err(ToolError::PermissionRejected);
        }
        PermissionDecision::Cancelled => {
            return Err(ToolError::permission_denied("Permission prompt was cancelled"));
        }
    }

    // Apply EOL normalization
    let normalized_content = normalize_eol(&request.content, sandbox_config.eol_policy);

    // Create parent directories if they don't exist
    if let Some(parent) = canonical_path.parent() {
        tokio::fs::create_dir_all(parent).await.map_err(|e| {
            ToolError::FileReadError {
                path: parent.display().to_string(),
                source: e,
            }
        })?;
    }

    // Perform atomic write
    write_atomic(&canonical_path, &normalized_content).await?;

    tracing::info!(
        path = %request.path,
        size = content_size,
        "File written successfully"
    );

    Ok(WriteTextFileResponse {})
}

/// Normalize line endings according to configured policy.
pub fn normalize_eol(content: &str, policy: EolPolicy) -> String {
    match policy {
        EolPolicy::Preserve => content.to_string(),
        EolPolicy::Lf => {
            // Replace CRLF with LF, then ensure all CR are removed
            content.replace("\r\n", "\n").replace('\r', "\n")
        }
        EolPolicy::Crlf => {
            // First normalize to LF, then replace with CRLF
            let lf_normalized = content.replace("\r\n", "\n").replace('\r', "\n");
            lf_normalized.replace('\n', "\r\n")
        }
    }
}

/// Write content to a file atomically using temp file + rename.
///
/// This minimizes the risk of partial writes by:
/// 1. Writing to a temporary file in the same directory
/// 2. Renaming the temp file to the target path
///
/// The rename operation is atomic on POSIX systems and near-atomic on Windows.
async fn write_atomic(path: &Path, content: &str) -> ToolResult<()> {
    // Create a temporary file in the same directory
    let parent = path.parent().unwrap_or_else(|| Path::new("."));
    let file_name = path.file_name().unwrap_or_default().to_string_lossy();
    let temp_path = parent.join(format!(".{}.tmp", file_name));

    // Write content to temporary file
    tokio::fs::write(&temp_path, content).await.map_err(|e| {
        ToolError::FileReadError {
            path: temp_path.display().to_string(),
            source: e,
        }
    })?;

    // Rename temp file to target (atomic operation)
    tokio::fs::rename(&temp_path, path).await.map_err(|e| {
        // Clean up temp file on error
        let _ = std::fs::remove_file(&temp_path);
        ToolError::FileReadError {
            path: path.display().to_string(),
            source: e,
        }
    })?;

    Ok(())
}

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

    #[test]
    fn test_normalize_eol_preserve() {
        let content = "line1\nline2\r\nline3\rline4";
        assert_eq!(normalize_eol(content, EolPolicy::Preserve), content);
    }

    #[test]
    fn test_normalize_eol_lf() {
        let content = "line1\nline2\r\nline3\rline4";
        let expected = "line1\nline2\nline3\nline4";
        assert_eq!(normalize_eol(content, EolPolicy::Lf), expected);
    }

    #[test]
    fn test_normalize_eol_crlf() {
        let content = "line1\nline2\r\nline3\rline4";
        let expected = "line1\r\nline2\r\nline3\r\nline4";
        assert_eq!(normalize_eol(content, EolPolicy::Crlf), expected);
    }

    #[test]
    fn test_normalize_eol_edge_cases() {
        // Empty string
        assert_eq!(normalize_eol("", EolPolicy::Lf), "");

        // Only newlines
        assert_eq!(normalize_eol("\n\n\n", EolPolicy::Crlf), "\r\n\r\n\r\n");

        // Mixed line endings to LF
        assert_eq!(normalize_eol("a\rb\nc\r\nd", EolPolicy::Lf), "a\nb\nc\nd");
    }
}
