//! MCP file-editing tools: `edit` and `write`.
//!
//! These are worktree-sandboxed equivalents of Claude's built-in `Edit` and
//! `Write` tools.  All paths must canonicalize to inside `.huskies/worktrees/`
//! so agents cannot write to the master working tree.

use crate::http::context::AppContext;
use serde_json::Value;
use std::path::{Path, PathBuf};

/// Validate that `file_path` is an absolute path whose nearest existing
/// ancestor lies inside the project's `.huskies/worktrees/` directory.
///
/// Unlike [`crate::service::shell::io::validate_working_dir`], the target file
/// itself need not exist (write creates it), so we walk up to the first
/// existing ancestor before canonicalising.
///
/// Returns the original (non-canonicalized) `PathBuf` on success so the
/// caller can use it directly for I/O.
///
/// # Errors
/// Returns a `String` error naming both the worktrees root and the offending
/// path, matching the style of the `run_command` guard.
pub(super) fn validate_worktree_file_path(
    file_path: &str,
    ctx: &AppContext,
) -> Result<PathBuf, String> {
    let path = PathBuf::from(file_path);

    if !path.is_absolute() {
        return Err(format!(
            "file_path must be an absolute path, got: {file_path}"
        ));
    }

    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
    let worktrees_root = project_root.join(".huskies").join("worktrees");

    if !worktrees_root.exists() {
        return Err(format!(
            "No worktrees directory found; file_path must be inside {worktrees_root:?}, got: {file_path}"
        ));
    }

    let canonical_wt = worktrees_root
        .canonicalize()
        .map_err(|e| format!("Cannot canonicalize worktrees root: {e}"))?;

    // Walk up to find the deepest existing ancestor so we can canonicalize it.
    let canonical_ancestor = find_existing_ancestor(&path)
        .ok_or_else(|| format!("file_path has no accessible ancestor on disk: {file_path}"))?
        .canonicalize()
        .map_err(|e| format!("Cannot canonicalize path: {e}"))?;

    if !canonical_ancestor.starts_with(&canonical_wt) {
        return Err(format!(
            "file_path must be inside worktrees root {worktrees_root:?}. Got: {file_path}"
        ));
    }

    Ok(path)
}

/// Return the deepest ancestor of `p` (inclusive) that exists on disk.
fn find_existing_ancestor(p: &Path) -> Option<&Path> {
    let mut current = p;
    loop {
        if current.exists() {
            return Some(current);
        }
        current = current.parent()?;
    }
}

/// Replace `old_string` with `new_string` in a file inside the agent's worktree.
///
/// Mirrors Claude's built-in `Edit` tool with worktree path validation.
/// By default replaces only the first occurrence; pass `replace_all: true`
/// to replace every occurrence.
pub(crate) fn tool_edit(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let file_path = args
        .get("file_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: file_path")?;
    let old_string = args
        .get("old_string")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: old_string")?;
    let new_string = args
        .get("new_string")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: new_string")?;
    let replace_all = args
        .get("replace_all")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);

    let path = validate_worktree_file_path(file_path, ctx)?;

    if !path.exists() {
        return Err(format!("file_path does not exist: {file_path}"));
    }

    let content =
        std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {file_path}: {e}"))?;

    if !content.contains(old_string) {
        return Err(format!(
            "old_string not found in {file_path}: {old_string:?}"
        ));
    }

    let new_content = if replace_all {
        content.replace(old_string, new_string)
    } else {
        content.replacen(old_string, new_string, 1)
    };

    std::fs::write(&path, &new_content).map_err(|e| format!("Failed to write {file_path}: {e}"))?;

    Ok(format!("Edited {file_path}"))
}

/// Write `content` to a file inside the agent's worktree, creating the file
/// (and any missing parent directories) if necessary.
///
/// Mirrors Claude's built-in `Write` tool with worktree path validation.
pub(crate) fn tool_write(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let file_path = args
        .get("file_path")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: file_path")?;
    let content = args
        .get("content")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: content")?;

    let path = validate_worktree_file_path(file_path, ctx)?;

    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("Failed to create parent dirs for {file_path}: {e}"))?;
    }

    std::fs::write(&path, content).map_err(|e| format!("Failed to write {file_path}: {e}"))?;

    Ok(format!("Written {file_path}"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::test_helpers::test_ctx;
    use serde_json::json;

    fn make_worktree(tmp: &tempfile::TempDir, name: &str) -> PathBuf {
        let wt = tmp.path().join(".huskies").join("worktrees").join(name);
        std::fs::create_dir_all(&wt).unwrap();
        wt
    }

    // ── validate_worktree_file_path ───────────────────────────────────

    #[test]
    fn validate_rejects_relative_path() {
        let tmp = tempfile::tempdir().unwrap();
        make_worktree(&tmp, "42_test");
        let ctx = test_ctx(tmp.path());
        let result = validate_worktree_file_path("relative/path.rs", &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("absolute"));
    }

    #[test]
    fn validate_rejects_path_outside_worktree() {
        let tmp = tempfile::tempdir().unwrap();
        make_worktree(&tmp, "42_test");
        let ctx = test_ctx(tmp.path());
        // /workspace/server/foo.rs is outside .huskies/worktrees/
        let outside = tmp.path().join("server").join("foo.rs");
        let result = validate_worktree_file_path(outside.to_str().unwrap(), &ctx);
        assert!(result.is_err(), "expected rejection, got ok");
        let msg = result.unwrap_err();
        assert!(
            msg.contains("worktrees"),
            "error should name worktrees root: {msg}"
        );
    }

    #[test]
    fn validate_accepts_existing_file_inside_worktree() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "42_test");
        let file = wt.join("foo.rs");
        std::fs::write(&file, "content").unwrap();
        let ctx = test_ctx(tmp.path());
        let result = validate_worktree_file_path(file.to_str().unwrap(), &ctx);
        assert!(result.is_ok(), "expected ok, got: {:?}", result);
    }

    #[test]
    fn validate_accepts_nonexistent_file_inside_worktree() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "42_test");
        // File doesn't exist yet — parent dir does
        let file = wt.join("new_file.rs");
        let ctx = test_ctx(tmp.path());
        let result = validate_worktree_file_path(file.to_str().unwrap(), &ctx);
        assert!(
            result.is_ok(),
            "expected ok for new file, got: {:?}",
            result
        );
    }

    #[test]
    fn validate_rejects_no_worktrees_dir() {
        let tmp = tempfile::tempdir().unwrap();
        // Do NOT create worktrees dir
        let ctx = test_ctx(tmp.path());
        let path = tmp.path().join("file.rs");
        let result = validate_worktree_file_path(path.to_str().unwrap(), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("worktrees"));
    }

    // ── tool_edit ─────────────────────────────────────────────────────

    /// AC3(a) — path outside worktree is rejected
    #[test]
    fn tool_edit_rejects_path_outside_worktree() {
        let tmp = tempfile::tempdir().unwrap();
        make_worktree(&tmp, "42_test");
        // Create a file outside worktrees
        let outside = tmp.path().join("server");
        std::fs::create_dir_all(&outside).unwrap();
        let outside_file = outside.join("foo.rs");
        std::fs::write(&outside_file, "old content").unwrap();
        let ctx = test_ctx(tmp.path());

        let result = tool_edit(
            &json!({
                "file_path": outside_file.to_str().unwrap(),
                "old_string": "old content",
                "new_string": "new content"
            }),
            &ctx,
        );
        assert!(result.is_err(), "expected rejection");
        // Master file unchanged
        let content = std::fs::read_to_string(&outside_file).unwrap();
        assert_eq!(content, "old content", "master file must be unchanged");
    }

    /// AC3(b) — path inside worktree succeeds
    #[test]
    fn tool_edit_accepts_path_inside_worktree() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "42_test");
        let file = wt.join("foo.rs");
        std::fs::write(&file, "fn old_fn() {}").unwrap();
        let ctx = test_ctx(tmp.path());

        let result = tool_edit(
            &json!({
                "file_path": file.to_str().unwrap(),
                "old_string": "old_fn",
                "new_string": "new_fn"
            }),
            &ctx,
        );
        assert!(result.is_ok(), "expected ok, got: {:?}", result);
        let content = std::fs::read_to_string(&file).unwrap();
        assert!(content.contains("new_fn"));
        assert!(!content.contains("old_fn"));
    }

    #[test]
    fn tool_edit_replace_all_replaces_every_occurrence() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "43_test");
        let file = wt.join("multi.rs");
        std::fs::write(&file, "foo foo foo").unwrap();
        let ctx = test_ctx(tmp.path());

        tool_edit(
            &json!({
                "file_path": file.to_str().unwrap(),
                "old_string": "foo",
                "new_string": "bar",
                "replace_all": true
            }),
            &ctx,
        )
        .unwrap();

        let content = std::fs::read_to_string(&file).unwrap();
        assert_eq!(content, "bar bar bar");
    }

    #[test]
    fn tool_edit_default_replaces_first_occurrence_only() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "44_test");
        let file = wt.join("single.rs");
        std::fs::write(&file, "foo foo foo").unwrap();
        let ctx = test_ctx(tmp.path());

        tool_edit(
            &json!({
                "file_path": file.to_str().unwrap(),
                "old_string": "foo",
                "new_string": "bar"
            }),
            &ctx,
        )
        .unwrap();

        let content = std::fs::read_to_string(&file).unwrap();
        assert_eq!(content, "bar foo foo");
    }

    #[test]
    fn tool_edit_fails_when_old_string_not_found() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "45_test");
        let file = wt.join("missing.rs");
        std::fs::write(&file, "hello world").unwrap();
        let ctx = test_ctx(tmp.path());

        let result = tool_edit(
            &json!({
                "file_path": file.to_str().unwrap(),
                "old_string": "not present",
                "new_string": "x"
            }),
            &ctx,
        );
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("not found"));
    }

    #[test]
    fn tool_edit_fails_when_file_does_not_exist() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "46_test");
        let ctx = test_ctx(tmp.path());

        let result = tool_edit(
            &json!({
                "file_path": wt.join("ghost.rs").to_str().unwrap(),
                "old_string": "x",
                "new_string": "y"
            }),
            &ctx,
        );
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("does not exist"));
    }

    // ── tool_write ────────────────────────────────────────────────────

    #[test]
    fn tool_write_rejects_path_outside_worktree() {
        let tmp = tempfile::tempdir().unwrap();
        make_worktree(&tmp, "42_test");
        let outside = tmp.path().join("master_file.rs");
        let ctx = test_ctx(tmp.path());

        let result = tool_write(
            &json!({
                "file_path": outside.to_str().unwrap(),
                "content": "evil"
            }),
            &ctx,
        );
        assert!(result.is_err(), "expected rejection");
        assert!(!outside.exists(), "master file must not be created");
    }

    #[test]
    fn tool_write_creates_new_file_inside_worktree() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "47_test");
        let file = wt.join("new.rs");
        let ctx = test_ctx(tmp.path());

        tool_write(
            &json!({
                "file_path": file.to_str().unwrap(),
                "content": "pub fn hello() {}"
            }),
            &ctx,
        )
        .unwrap();

        let content = std::fs::read_to_string(&file).unwrap();
        assert_eq!(content, "pub fn hello() {}");
    }

    #[test]
    fn tool_write_overwrites_existing_file() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "48_test");
        let file = wt.join("existing.rs");
        std::fs::write(&file, "old").unwrap();
        let ctx = test_ctx(tmp.path());

        tool_write(
            &json!({
                "file_path": file.to_str().unwrap(),
                "content": "new"
            }),
            &ctx,
        )
        .unwrap();

        let content = std::fs::read_to_string(&file).unwrap();
        assert_eq!(content, "new");
    }

    #[test]
    fn tool_write_creates_parent_dirs() {
        let tmp = tempfile::tempdir().unwrap();
        let wt = make_worktree(&tmp, "49_test");
        let file = wt.join("deep").join("nested").join("file.rs");
        let ctx = test_ctx(tmp.path());

        tool_write(
            &json!({
                "file_path": file.to_str().unwrap(),
                "content": "deep content"
            }),
            &ctx,
        )
        .unwrap();

        let content = std::fs::read_to_string(&file).unwrap();
        assert_eq!(content, "deep content");
    }

    #[test]
    fn tool_write_missing_content_arg_errors() {
        let tmp = tempfile::tempdir().unwrap();
        make_worktree(&tmp, "50_test");
        let ctx = test_ctx(tmp.path());

        let result = tool_write(&json!({"file_path": "/some/path"}), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("content"));
    }
}
