use std::io::{Read, Write};
use std::sync::Arc;

use schemars::JsonSchema;
use serde::Deserialize;

use crate::{AsArc, Sandbox, ToolError, ToolResult, sandbox::map_io_err};

use crate::tool_utils::{self as fs_utils, deserialize_bool_flexible};

use super::Tool;

#[derive(Debug, Deserialize, JsonSchema)]
pub struct EditParams {
    /// Path to the file to edit, relative to the sandbox root.
    pub path: String,
    /// Exact string to search for. Must appear exactly once unless replace_all is true.
    /// Include enough surrounding context to uniquely identify the location.
    pub old_string: String,
    /// String to replace it with.
    pub new_string: String,
    /// If true, replace every occurrence instead of requiring exactly one. Defaults to false.
    #[serde(default, deserialize_with = "deserialize_bool_flexible")]
    pub replace_all: bool,
}

pub struct EditFileTool;

impl Tool for EditFileTool {
    type Params = EditParams;
    const NAME: &'static str = "edit_file";
    const DESCRIPTION: &'static str = "Replace text within a file. Reads the file, finds the exact string provided in 'old_string', \
         and replaces it with 'new_string'. By default the string must appear exactly once — include \
         enough surrounding context in 'old_string' to make it unique. \
         If the string appears multiple times and you want to replace all of them, set replace_all to true. \
         Always read the file before editing to ensure 'old_string' matches the current content exactly.";

    async fn run(sandbox: impl AsArc<Sandbox> + Send, params: EditParams) -> ToolResult<String> {
        // The blocking syscalls (open, read_to_end, create, write_all) run
        // inline: they are short enough that dispatching to a blocking pool
        // costs more than it saves.
        do_edit(sandbox.as_arc(), params)
    }
}

fn do_edit(sandbox: Arc<Sandbox>, mut params: EditParams) -> ToolResult<String> {
    params.path = fs_utils::normalize_path(&params.path).to_string();
    let mut fs_config = sandbox.files.write().unwrap();
    let root = fs_config.root.as_ref().expect("Missing sandbox root");

    // Enforce read-before-write and check for external modifications.
    let current_mtime = root
        .metadata(&params.path)
        .map_err(|e| map_io_err(&params.path, e))?
        .modified()
        .map_err(ToolError::IoError)?;
    fs_utils::check_write_allowed(&fs_config.read_registry, &params.path, current_mtime)?;

    // SANDBOX: fd-relative open enforced by cap-std / RESOLVE_BENEATH.
    let mut file = root
        .open(&params.path)
        .map_err(|e| map_io_err(&params.path, e))?;

    // `cap_std::fs::File` implements `std::io::Read` by calling the `read`
    // syscall on the already-open file descriptor. No path resolution occurs —
    // the sandbox guarantee was established at `open()` time above.
    let mut buf = Vec::new();
    file.read_to_end(&mut buf).map_err(ToolError::IoError)?;
    let content = String::from_utf8(buf).map_err(|_| ToolError::NotUtf8(params.path.clone()))?;

    // Validate — pure in-memory string operations, no filesystem access.
    let count = content.matches(params.old_string.as_str()).count();
    match count {
        0 => return Err(ToolError::StringNotFound(params.path.clone())),
        n if n > 1 && !params.replace_all => {
            return Err(ToolError::AmbiguousEdit {
                path: params.path.clone(),
                count: n,
            });
        }
        _ => {}
    }

    // Replace — pure in-memory, no filesystem access.
    let updated = if params.replace_all {
        content.replace(params.old_string.as_str(), &params.new_string)
    } else {
        content.replacen(params.old_string.as_str(), &params.new_string, 1)
    };

    // SANDBOX: fd-relative create enforced by cap-std / RESOLVE_BENEATH.
    let mut file = root
        .create(&params.path)
        .map_err(|e| map_io_err(&params.path, e))?;

    // `cap_std::fs::File` implements `std::io::Write` by calling the `write`
    // syscall on the already-open file descriptor. No path resolution occurs —
    // the sandbox guarantee was established at `create()` time above.
    file.write_all(updated.as_bytes())
        .map_err(ToolError::IoError)?;

    // Update the registry with the new mtime so subsequent writes are allowed.
    fs_utils::update_registry(&mut fs_config.read_registry, &params.path, &file)?;

    let replacements = if params.replace_all { count } else { 1 };
    Ok(format!(
        "Replaced {} occurrence(s) in '{}'",
        replacements, params.path
    ))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Sandbox;
    use crate::tools::read::{ReadFileTool, ReadParams};
    use std::fs;
    use tempfile::TempDir;

    fn setup() -> (TempDir, Arc<Sandbox>) {
        let dir = TempDir::new().unwrap();
        let sandbox = Arc::new(Sandbox::new());
        sandbox.enable_fs(dir.path()).unwrap();
        (dir, sandbox)
    }

    async fn read(sandbox: impl AsArc<Sandbox> + Send, path: &str) {
        ReadFileTool::run(
            sandbox,
            ReadParams {
                path: path.into(),
                limit: None,
                offset: None,
                max_bytes: None,
            },
        )
        .await
        .unwrap();
    }

    #[tokio::test]
    async fn replaces_string() {
        let (dir, sandbox) = setup();
        fs::write(dir.path().join("f.txt"), "hello world").unwrap();
        read(Arc::clone(&sandbox), "f.txt").await;

        EditFileTool::run(
            Arc::clone(&sandbox),
            EditParams {
                path: "f.txt".into(),
                old_string: "world".into(),
                new_string: "rust".into(),
                replace_all: false,
            },
        )
        .await
        .unwrap();

        assert_eq!(
            fs::read_to_string(dir.path().join("f.txt")).unwrap(),
            "hello rust"
        );
    }

    #[tokio::test]
    async fn replace_all_replaces_every_occurrence() {
        let (dir, sandbox) = setup();
        fs::write(dir.path().join("f.txt"), "a a a").unwrap();
        read(Arc::clone(&sandbox), "f.txt").await;

        EditFileTool::run(
            Arc::clone(&sandbox),
            EditParams {
                path: "f.txt".into(),
                old_string: "a".into(),
                new_string: "b".into(),
                replace_all: true,
            },
        )
        .await
        .unwrap();

        assert_eq!(
            fs::read_to_string(dir.path().join("f.txt")).unwrap(),
            "b b b"
        );
    }

    #[tokio::test]
    async fn leading_slash_is_stripped() {
        let (dir, sandbox) = setup();
        fs::write(dir.path().join("f.txt"), "hello world").unwrap();
        read(Arc::clone(&sandbox), "/f.txt").await;

        EditFileTool::run(
            &sandbox,
            EditParams {
                path: "/f.txt".into(),
                old_string: "world".into(),
                new_string: "rust".into(),
                replace_all: false,
            },
        )
        .await
        .unwrap();

        assert_eq!(
            fs::read_to_string(dir.path().join("f.txt")).unwrap(),
            "hello rust"
        );
    }

    #[tokio::test]
    async fn errors_on_ambiguous_match() {
        let (dir, sandbox) = setup();
        fs::write(dir.path().join("f.txt"), "a a").unwrap();
        read(&sandbox, "f.txt").await;

        let result = EditFileTool::run(
            &sandbox,
            EditParams {
                path: "f.txt".into(),
                old_string: "a".into(),
                new_string: "b".into(),
                replace_all: false,
            },
        )
        .await;

        assert!(matches!(result, Err(ToolError::AmbiguousEdit { .. })));
    }

    #[tokio::test]
    async fn errors_on_string_not_found() {
        let (dir, sandbox) = setup();
        fs::write(dir.path().join("f.txt"), "hello").unwrap();
        read(&sandbox, "f.txt").await;

        let result = EditFileTool::run(
            &sandbox,
            EditParams {
                path: "f.txt".into(),
                old_string: "xyzzy".into(),
                new_string: "b".into(),
                replace_all: false,
            },
        )
        .await;

        assert!(matches!(result, Err(ToolError::StringNotFound(_))));
    }

    #[tokio::test]
    async fn errors_without_prior_read() {
        let (dir, sandbox) = setup();
        fs::write(dir.path().join("f.txt"), "hello").unwrap();

        let result = EditFileTool::run(
            &sandbox,
            EditParams {
                path: "f.txt".into(),
                old_string: "hello".into(),
                new_string: "world".into(),
                replace_all: false,
            },
        )
        .await;

        assert!(matches!(result, Err(ToolError::NotRead(_))));
    }

    use super::EditParams;

    #[test]
    fn deserialize_bool_flexible() {
        fn parse(replace_all: &str) -> EditParams {
            serde_json::from_str(&format!(
                r#"{{"path":"f","old_string":"a","new_string":"b","replace_all":{replace_all}}}"#
            ))
            .unwrap()
        }

        // JSON booleans
        assert!(parse("true").replace_all);
        assert!(!parse("false").replace_all);

        // String booleans (sent by some MCP clients)
        assert!(parse(r#""true""#).replace_all);
        assert!(!parse(r#""false""#).replace_all);

        // Absent field defaults to false
        let p: EditParams =
            serde_json::from_str(r#"{"path":"f","old_string":"a","new_string":"b"}"#).unwrap();
        assert!(!p.replace_all);

        // Invalid string is rejected
        let result: serde_json::Result<EditParams> = serde_json::from_str(
            r#"{"path":"f","old_string":"a","new_string":"b","replace_all":"yes"}"#,
        );
        assert!(result.is_err());
    }
}
