//! Internal edit helper for read + transform + write operations.
//!
//! **Status**: Implemented (TOOLS-FS-04)
//!
//! This module implements:
//! - Edit operation abstraction (not ACP-native, internal API)
//! - Read + transform + write flow
//! - Automatic diff generation
//! - String replacement operations

use crate::config::SandboxConfig;
use crate::error::{ToolError, ToolResult};
use crate::fs::diff::generate_diff;
use crate::fs::read::{read_text_file, ReadTextFileRequest};
use crate::fs::write::{write_text_file, WriteTextFileRequest};
use crate::path::validate_path;
use serde::{Deserialize, Serialize};

/// Request to edit a file via transformation operations.
///
/// **Note**: This is an internal API, not exposed via ACP directly.
/// Agents use fs/write_text_file; this is for richer Dirigent UX.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditFileRequest {
    /// Absolute path to the file to edit.
    pub path: String,

    /// Ordered list of edit operations to apply.
    pub edits: Vec<EditOperation>,
}

/// A single edit operation to apply to file content.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EditOperation {
    /// Replace occurrences of old_text with new_text.
    Replace {
        /// Text to find.
        old_text: String,

        /// Text to replace with.
        new_text: String,

        /// Replace all occurrences (true) or first only (false).
        replace_all: bool,
    },

    /// Apply a unified diff patch (future).
    #[allow(dead_code)]
    Patch {
        /// Unified diff string.
        diff: String,
    },
}

/// Response from editing a file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditFileResponse {
    /// Unified diff showing changes made.
    pub diff: String,
}

/// Edit a file by applying transformation operations.
///
/// ## Implementation
///
/// This function:
/// 1. Reads existing file content
/// 2. Applies edits in order:
///    - Replace: String find/replace (once or all)
///    - Patch: Apply unified diff (future)
/// 3. Writes transformed content
/// 4. Generates and returns diff
///
/// ## Algorithm
///
/// ```text
/// 1. old_content = read_text_file(path)
/// 2. new_content = old_content
/// 3. for each edit in edits:
///      new_content = apply_edit(new_content, edit)
/// 4. write_text_file(path, new_content)
/// 5. diff = generate_diff(old_content, new_content, path)
/// 6. return EditFileResponse { diff }
/// ```
///
/// ## Error Cases
///
/// - File not found → `ToolError::NotFound`
/// - Edit on new file → `ToolError::NotFound` (edits require existing content)
/// - Same sandboxing/permission errors as read/write
///
/// ## Tool Call Rendering
///
/// Always emits `ToolCallContent::diff` for UX visualization.
///
/// ## See Also
///
/// - Task spec: `docs/building/04_acp_client/04_tasks_02_tools_and_sandboxing.md#TOOLS-FS-04`
pub async fn edit_file(
    request: EditFileRequest,
    sandbox_config: &SandboxConfig,
    permission_config: &crate::config::PermissionConfig,
    permission_context: &crate::permission::check::PermissionContext,
) -> ToolResult<EditFileResponse> {
    // Validate path first to get canonical path for diff generation
    let canonical_path = validate_path(&request.path, sandbox_config)?;

    // Read existing file content
    let read_request = ReadTextFileRequest {
        path: request.path.clone(),
        line: None,
        limit: None,
    };

    let read_response = read_text_file(read_request, sandbox_config).await?;
    let old_content = read_response.content;

    // Apply all edits in order
    let mut new_content = old_content.clone();
    for edit in &request.edits {
        new_content = apply_edit(&new_content, edit)?;
    }

    // Write transformed content
    let write_request = WriteTextFileRequest {
        path: request.path.clone(),
        content: new_content.clone(),
    };

    write_text_file(write_request, sandbox_config, permission_config, permission_context).await?;

    // Generate diff for UX
    let diff = generate_diff(&old_content, &new_content, &canonical_path);

    tracing::info!(
        path = %request.path,
        edit_count = request.edits.len(),
        "File edited successfully"
    );

    Ok(EditFileResponse { diff })
}

/// Apply a single edit operation to content.
fn apply_edit(content: &str, edit: &EditOperation) -> ToolResult<String> {
    match edit {
        EditOperation::Replace {
            old_text,
            new_text,
            replace_all,
        } => {
            if *replace_all {
                // Replace all occurrences
                Ok(content.replace(old_text, new_text))
            } else {
                // Replace only the first occurrence
                if let Some(pos) = content.find(old_text) {
                    let mut result = String::with_capacity(content.len());
                    result.push_str(&content[..pos]);
                    result.push_str(new_text);
                    result.push_str(&content[pos + old_text.len()..]);
                    Ok(result)
                } else {
                    // No match found - return content unchanged
                    // This could be a warning, but we'll allow it
                    tracing::warn!(
                        old_text = %old_text,
                        "Edit operation: old_text not found in content"
                    );
                    Ok(content.to_string())
                }
            }
        }
        EditOperation::Patch { diff: _ } => {
            // Future: Apply unified diff patch
            Err(ToolError::InvalidInput(
                "Patch edit operation not yet implemented".to_string(),
            ))
        }
    }
}

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

    #[test]
    fn test_apply_edit_replace_all() {
        let content = "foo bar foo baz foo";
        let edit = EditOperation::Replace {
            old_text: "foo".to_string(),
            new_text: "qux".to_string(),
            replace_all: true,
        };
        let result = apply_edit(content, &edit).unwrap();
        assert_eq!(result, "qux bar qux baz qux");
    }

    #[test]
    fn test_apply_edit_replace_first() {
        let content = "foo bar foo baz foo";
        let edit = EditOperation::Replace {
            old_text: "foo".to_string(),
            new_text: "qux".to_string(),
            replace_all: false,
        };
        let result = apply_edit(content, &edit).unwrap();
        assert_eq!(result, "qux bar foo baz foo");
    }

    #[test]
    fn test_apply_edit_no_match() {
        let content = "foo bar baz";
        let edit = EditOperation::Replace {
            old_text: "qux".to_string(),
            new_text: "quux".to_string(),
            replace_all: false,
        };
        let result = apply_edit(content, &edit).unwrap();
        assert_eq!(result, content); // Unchanged
    }

    #[test]
    fn test_apply_edit_empty_replacement() {
        let content = "foo bar foo";
        let edit = EditOperation::Replace {
            old_text: "foo".to_string(),
            new_text: "".to_string(),
            replace_all: true,
        };
        let result = apply_edit(content, &edit).unwrap();
        assert_eq!(result, " bar ");
    }

    #[test]
    fn test_apply_edit_multiline() {
        let content = "line1\nfoo\nline3\nfoo\nline5";
        let edit = EditOperation::Replace {
            old_text: "foo".to_string(),
            new_text: "bar".to_string(),
            replace_all: true,
        };
        let result = apply_edit(content, &edit).unwrap();
        assert_eq!(result, "line1\nbar\nline3\nbar\nline5");
    }

    #[test]
    fn test_apply_edit_patch_unimplemented() {
        let content = "foo bar baz";
        let edit = EditOperation::Patch {
            diff: "some diff".to_string(),
        };
        let result = apply_edit(content, &edit);
        assert!(result.is_err());
        assert!(matches!(result, Err(ToolError::InvalidInput(_))));
    }
}
