//! MCP merge tools — merge agent work to master and report merge failures.
use crate::agents::move_story_to_merge;
use crate::http::context::AppContext;
use crate::io::story_metadata::write_merge_failure;
use crate::slog;
use crate::slog_warn;
use serde_json::{Value, json};

pub(super) async fn tool_merge_agent_work(
    args: &Value,
    ctx: &AppContext,
) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    // Check CRDT stage before attempting merge — if already done or archived,
    // return success immediately to avoid spurious error notifications.
    if let Some(item) = crate::crdt_state::read_item(story_id)
        && crate::pipeline_state::Stage::from_dir(&item.stage).is_some_and(|s| {
            matches!(
                s,
                crate::pipeline_state::Stage::Done { .. }
                    | crate::pipeline_state::Stage::Archived { .. }
            )
        })
    {
        return serde_json::to_string_pretty(&json!({
            "story_id": story_id,
            "status": "completed",
            "success": true,
            "message": format!(
                "Story '{}' is already in '{}' — no merge needed.",
                story_id, item.stage
            ),
        }))
        .map_err(|e| format!("Serialization error: {e}"));
    }

    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
    ctx.services
        .agents
        .start_merge_agent_work(&project_root, story_id)?;

    // Block until the merge completes instead of returning immediately.
    // Uses tokio::time::sleep so the async executor is not blocked.
    // This prevents the mergemaster from burning all its turns polling
    // get_merge_status in a tight loop.
    let sid = story_id.to_string();
    let agents = ctx.services.agents.clone();
    loop {
        tokio::time::sleep(std::time::Duration::from_secs(10)).await;
        if let Some(job) = agents.get_merge_status(&sid) {
            match &job.status {
                crate::agents::merge::MergeJobStatus::Running => continue,
                _ => break,
            }
        } else {
            return Err(format!("Merge job disappeared for '{sid}'."));
        }
    }

    // Return the full result (same fields as get_merge_status) so the caller
    // has everything it needs without a second round-trip.
    tool_get_merge_status(args, ctx)
}

pub(super) fn tool_get_merge_status(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    let job = ctx
        .services
        .agents
        .get_merge_status(story_id)
        .ok_or_else(|| {
            format!("No merge job found for story '{story_id}'. Call merge_agent_work first.")
        })?;

    match &job.status {
        crate::agents::merge::MergeJobStatus::Running => {
            serde_json::to_string_pretty(&json!({
                "story_id": story_id,
                "status": "running",
                "message": "Merge pipeline is still running."
            }))
            .map_err(|e| format!("Serialization error: {e}"))
        }
        crate::agents::merge::MergeJobStatus::Completed(report) => {
            let status_msg = if report.success && report.gates_passed && report.conflicts_resolved {
                "Merge complete: conflicts were auto-resolved and all quality gates passed. Story moved to done and worktree cleaned up."
            } else if report.success && report.gates_passed {
                "Merge complete: all quality gates passed. Story moved to done and worktree cleaned up."
            } else if report.had_conflicts && !report.conflicts_resolved {
                "Merge failed: conflicts detected that could not be auto-resolved. Merge was aborted — master is untouched. Call report_merge_failure with the conflict details so the human can resolve them. Do NOT manually move the story file or call accept_story."
            } else if report.success && !report.gates_passed {
                "Merge committed but quality gates failed. Review gate_output and fix issues before re-running."
            } else {
                "Merge failed. Review gate_output for details. Call report_merge_failure to record the failure. Do NOT manually move the story file or call accept_story."
            };

            serde_json::to_string_pretty(&json!({
                "story_id": story_id,
                "status": "completed",
                "success": report.success,
                "had_conflicts": report.had_conflicts,
                "conflicts_resolved": report.conflicts_resolved,
                "conflict_details": report.conflict_details,
                "gates_passed": report.gates_passed,
                "gate_output": report.gate_output,
                "worktree_cleaned_up": report.worktree_cleaned_up,
                "story_archived": report.story_archived,
                "message": status_msg,
            }))
            .map_err(|e| format!("Serialization error: {e}"))
        }
        crate::agents::merge::MergeJobStatus::Failed(err) => {
            serde_json::to_string_pretty(&json!({
                "story_id": story_id,
                "status": "failed",
                "error": err,
                "message": format!("Merge pipeline failed: {err}. Call report_merge_failure to record the failure.")
            }))
            .map_err(|e| format!("Serialization error: {e}"))
        }
    }
}

pub(super) async fn tool_move_story_to_merge(
    args: &Value,
    ctx: &AppContext,
) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;
    let agent_name = args
        .get("agent_name")
        .and_then(|v| v.as_str())
        .unwrap_or("mergemaster");

    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;

    // Move story from work/2_current/ to work/4_merge/
    move_story_to_merge(&project_root, story_id)?;

    // Start the mergemaster agent on the story worktree
    let info = ctx
        .services
        .agents
        .start_agent(&project_root, story_id, Some(agent_name), None, None)
        .await?;

    serde_json::to_string_pretty(&json!({
        "story_id": info.story_id,
        "agent_name": info.agent_name,
        "status": info.status.to_string(),
        "worktree_path": info.worktree_path,
        "message": format!(
            "Story '{story_id}' moved to work/4_merge/ and mergemaster agent '{}' started.",
            info.agent_name
        ),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

pub(super) fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let story_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;
    let reason = args
        .get("reason")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: reason")?;

    slog!("[mergemaster] Merge failure reported for '{story_id}': {reason}");
    ctx.services.agents.set_merge_failure_reported(story_id);

    // Broadcast the failure so the Matrix notification listener can post an
    // error message to configured rooms without coupling this tool to the bot.
    let _ = ctx
        .watcher_tx
        .send(crate::io::watcher::WatcherEvent::MergeFailure {
            story_id: story_id.to_string(),
            reason: reason.to_string(),
        });

    // Persist the failure reason to the story file's front matter so it
    // survives server restarts and is visible in the web UI.
    if let Ok(project_root) = ctx.state.get_project_root() {
        let story_file = project_root
            .join(".huskies")
            .join("work")
            .join("4_merge")
            .join(format!("{story_id}.md"));
        if story_file.exists() {
            if let Err(e) = write_merge_failure(&story_file, reason) {
                slog_warn!(
                    "[mergemaster] Failed to persist merge_failure to story file for '{story_id}': {e}"
                );
            }
        } else {
            slog_warn!(
                "[mergemaster] Story file not found in 4_merge/ for '{story_id}'; \
                 merge_failure not persisted to front matter"
            );
        }
    }

    Ok(format!(
        "Merge failure for '{story_id}' recorded. Story remains in work/4_merge/. Reason: {reason}"
    ))
}

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

    fn setup_git_repo_in(dir: &std::path::Path) {
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(dir)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(dir)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(dir)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "--allow-empty", "-m", "init"])
            .current_dir(dir)
            .output()
            .unwrap();
    }

    #[test]
    fn merge_agent_work_in_tools_list() {
        use super::super::handle_tools_list;
        let resp = handle_tools_list(Some(json!(1)));
        let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
        let tool = tools.iter().find(|t| t["name"] == "merge_agent_work");
        assert!(tool.is_some(), "merge_agent_work missing from tools list");
        let t = tool.unwrap();
        assert!(t["description"].is_string());
        let required = t["inputSchema"]["required"].as_array().unwrap();
        let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
        assert!(req_names.contains(&"story_id"));
        // agent_name is optional
        assert!(!req_names.contains(&"agent_name"));
    }

    #[test]
    fn move_story_to_merge_in_tools_list() {
        use super::super::handle_tools_list;
        let resp = handle_tools_list(Some(json!(1)));
        let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
        let tool = tools.iter().find(|t| t["name"] == "move_story_to_merge");
        assert!(
            tool.is_some(),
            "move_story_to_merge missing from tools list"
        );
        let t = tool.unwrap();
        assert!(t["description"].is_string());
        let required = t["inputSchema"]["required"].as_array().unwrap();
        let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
        assert!(req_names.contains(&"story_id"));
        // agent_name is optional
        assert!(!req_names.contains(&"agent_name"));
    }

    #[tokio::test]
    async fn tool_merge_agent_work_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_merge_agent_work(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_merge_agent_work_already_done_returns_success() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item(
            "99_story_already_done",
            "5_done",
            Some("Already done story"),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result =
            tool_merge_agent_work(&json!({"story_id": "99_story_already_done"}), &ctx).await;
        assert!(result.is_ok(), "expected Ok, got: {result:?}");
        let body = result.unwrap();
        let v: serde_json::Value = serde_json::from_str(&body).unwrap();
        assert_eq!(v["status"], "completed");
        assert_eq!(v["success"], true);
        assert!(v["message"].as_str().unwrap().contains("5_done"));
    }

    #[tokio::test]
    async fn tool_merge_agent_work_already_archived_returns_success() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item(
            "98_story_already_archived",
            "6_archived",
            Some("Already archived story"),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result =
            tool_merge_agent_work(&json!({"story_id": "98_story_already_archived"}), &ctx).await;
        assert!(result.is_ok(), "expected Ok, got: {result:?}");
        let body = result.unwrap();
        let v: serde_json::Value = serde_json::from_str(&body).unwrap();
        assert_eq!(v["status"], "completed");
        assert_eq!(v["success"], true);
        assert!(v["message"].as_str().unwrap().contains("6_archived"));
    }

    #[tokio::test]
    async fn tool_move_story_to_merge_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_move_story_to_merge(&json!({}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async fn tool_move_story_to_merge_moves_file() {
        let tmp = tempfile::tempdir().unwrap();
        setup_git_repo_in(tmp.path());
        let current_dir = tmp.path().join(".huskies/work/2_current");
        std::fs::create_dir_all(&current_dir).unwrap();
        let content = "---\nname: Test\n---\n";
        let story_file = current_dir.join("24_story_test.md");
        std::fs::write(&story_file, content).unwrap();
        crate::db::ensure_content_store();
        crate::db::write_content("24_story_test", content);
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(tmp.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "add story"])
            .current_dir(tmp.path())
            .output()
            .unwrap();

        let ctx = test_ctx(tmp.path());
        // The agent start will fail in test (no worktree/config), but the move should succeed
        let result = tool_move_story_to_merge(&json!({"story_id": "24_story_test"}), &ctx).await;
        // Content store should still have the item after the move
        assert!(
            crate::db::read_content("24_story_test").is_some(),
            "content store should have the story after move"
        );
        // Result is either Ok (agent started) or Err (agent failed - acceptable in tests)
        let _ = result;
    }

    // tool_merge_agent_work_returns_started removed: the function now blocks
    // in a poll loop until the merge completes, so it can't be tested without
    // a full merge pipeline. The blocking behaviour is tested via integration.

    #[test]
    fn tool_get_merge_status_no_job() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_get_merge_status(&json!({"story_id": "99_nonexistent"}), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("No merge job"));
    }

    // tool_get_merge_status_returns_running removed: depends on
    // tool_merge_agent_work which now blocks indefinitely in a poll loop.

    #[test]
    fn report_merge_failure_in_tools_list() {
        use super::super::handle_tools_list;
        let resp = handle_tools_list(Some(json!(1)));
        let tools = resp.result.unwrap()["tools"].as_array().unwrap().clone();
        let tool = tools.iter().find(|t| t["name"] == "report_merge_failure");
        assert!(
            tool.is_some(),
            "report_merge_failure missing from tools list"
        );
        let t = tool.unwrap();
        assert!(t["description"].is_string());
        let required = t["inputSchema"]["required"].as_array().unwrap();
        let req_names: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
        assert!(req_names.contains(&"story_id"));
        assert!(req_names.contains(&"reason"));
    }

    #[test]
    fn tool_report_merge_failure_missing_story_id() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_report_merge_failure(&json!({"reason": "conflicts"}), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[test]
    fn tool_report_merge_failure_missing_reason() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_report_merge_failure(&json!({"story_id": "42_story_foo"}), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("reason"));
    }

    #[test]
    fn tool_report_merge_failure_returns_confirmation() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_report_merge_failure(
            &json!({
                "story_id": "42_story_foo",
                "reason": "Unresolvable merge conflicts in src/main.rs"
            }),
            &ctx,
        );
        assert!(result.is_ok());
        let msg = result.unwrap();
        assert!(msg.contains("42_story_foo"));
        assert!(msg.contains("work/4_merge/"));
        assert!(msg.contains("Unresolvable merge conflicts"));
    }
}
