//! 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::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)
        && matches!(
            item.stage(),
            crate::pipeline_state::Stage::Done { .. }
                | crate::pipeline_state::Stage::Archived { .. }
                | crate::pipeline_state::Stage::Abandoned { .. }
                | crate::pipeline_state::Stage::Superseded { .. }
                | crate::pipeline_state::Stage::Rejected { .. }
        )
    {
        let stage_name = item.stage().dir_name().to_string();
        return serde_json::to_string_pretty(&json!({
            "story_id": story_id,
            "status": "completed",
            "success": true,
            "message": format!(
                "Story '{story_id}' is already in '{stage_name}' — no merge needed.",
            ),
        }))
        .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 => {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs_f64();
            let elapsed_seconds = (now - job.started_at).max(0.0);
            serde_json::to_string_pretty(&json!({
                "story_id": story_id,
                "status": "running",
                "elapsed_seconds": elapsed_seconds,
                "message": "Merge pipeline is still running."
            }))
            .map_err(|e| format!("Serialization error: {e}"))
        }
        crate::agents::merge::MergeJobStatus::Completed(report) => {
            use crate::agents::merge::MergeResult;
            let status_msg = crate::service::merge::format_merge_status_message(report);
            let (success, had_conflicts, conflicts_resolved, conflict_details, gates_passed) =
                match &report.result {
                    MergeResult::Success { conflicts_resolved, conflict_details, .. } => {
                        (true, *conflicts_resolved, *conflicts_resolved, conflict_details.clone(), true)
                    }
                    MergeResult::Conflict { details, .. } => {
                        (false, true, false, details.clone(), false)
                    }
                    MergeResult::GateFailure { .. } => {
                        (false, false, false, None, false)
                    }
                    MergeResult::NoCommits { .. } => {
                        (false, false, false, None, false)
                    }
                    MergeResult::Other { conflict_details, .. } => {
                        (false, false, false, conflict_details.clone(), false)
                    }
                };
            // AC1: condensed digest instead of the full (potentially 35KB+)
            // gate_output — successful merges carry no summary at all.
            let gate_summary = crate::service::merge::summarize_merge_result(&report.result);

            serde_json::to_string_pretty(&json!({
                "story_id": story_id,
                "status": "completed",
                "success": success,
                "had_conflicts": had_conflicts,
                "conflicts_resolved": conflicts_resolved,
                "conflict_details": conflict_details,
                "gates_passed": gates_passed,
                "gate_summary": if gate_summary.is_empty() { serde_json::Value::Null } else { json!(gate_summary) },
                "report_path": report.report_path,
                "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 req = crate::validation::MoveStoryToMergeRequest::from_json(args)?;
    let story_id = req.story_id.as_str();
    let agent_name = req.resolved_agent_name();

    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(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}"))
}

/// Record a merge failure reported by the mergemaster agent.
pub(super) async 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)
        .await;

    // The mergemaster provides a freeform reason string; use Other so the
    // auto-assigner does not re-spawn another mergemaster after this one fails.
    let kind = crate::pipeline_state::MergeFailureKind::Other(reason.to_string());
    let display = kind.display_reason();

    // Route the failure through the typed state machine (Merge → MergeFailure).
    // Only broadcast the notification when the stage actually changed; if the
    // story was already in MergeFailure (self-loop), suppress the duplicate.
    let should_notify = match crate::agents::lifecycle::transition_to_merge_failure(story_id, kind)
    {
        Ok(fired) => !matches!(
            fired.before,
            crate::pipeline_state::Stage::MergeFailure { .. }
        ),
        Err(e) => {
            slog_warn!("[mergemaster] Failed to transition '{story_id}' to MergeFailure: {e}");
            true
        }
    };
    if should_notify {
        let _ = ctx
            .watcher_tx
            .send(crate::io::watcher::WatcherEvent::MergeFailure {
                story_id: story_id.to_string(),
                reason: display,
            });
    }

    Ok(format!(
        "Merge failure for '{story_id}' recorded. 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_str(
            "99_story_already_done",
            "5_done",
            Some("Already done story"),
            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("done"));
    }

    #[tokio::test]
    async fn tool_merge_agent_work_already_archived_returns_success() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_item_str(
            "98_story_already_archived",
            "6_archived",
            Some("Already archived story"),
            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("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(crate::db::ContentKey::Story("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(crate::db::ContentKey::Story("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"));
    }

    /// AC3: a still-running merge job returns status and elapsed time only —
    /// no gate data of any kind belongs on a Running response.
    #[test]
    fn tool_get_merge_status_running_includes_elapsed_seconds_only() {
        crate::crdt_state::init_for_test();
        crate::crdt_state::write_merge_job(
            "50_story_running",
            "running",
            0.0,
            None,
            Some("{\"server_start\":0.0}"),
        );
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_get_merge_status(&json!({"story_id": "50_story_running"}), &ctx);
        assert!(result.is_ok(), "expected Ok, got: {result:?}");
        let v: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
        assert_eq!(v["status"], "running");
        assert!(
            v["elapsed_seconds"].as_f64().unwrap() > 0.0,
            "elapsed_seconds must reflect time since started_at=0.0: {v}"
        );
        assert!(
            v.get("gate_output").is_none() && v.get("gate_summary").is_none(),
            "a running job must carry no gate data: {v}"
        );
    }

    /// AC1: a successful merge's response carries no gate summary at all.
    #[test]
    fn tool_get_merge_status_completed_success_has_no_gate_summary() {
        crate::crdt_state::init_for_test();
        let report = crate::agents::merge::MergeReport {
            story_id: "51_story_ok".to_string(),
            result: crate::agents::merge::MergeResult::Success {
                conflicts_resolved: false,
                conflict_details: None,
                gate_output: "all tests passed".to_string(),
            },
            worktree_cleaned_up: true,
            story_archived: true,
            report_path: Some(".huskies/merge_reports/51_story_ok-1.log".to_string()),
        };
        let report_json = serde_json::to_string(&report).unwrap();
        crate::crdt_state::write_merge_job(
            "51_story_ok",
            "completed",
            1.0,
            Some(2.0),
            Some(&report_json),
        );

        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_get_merge_status(&json!({"story_id": "51_story_ok"}), &ctx).unwrap();
        let v: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert!(
            v["gate_summary"].is_null(),
            "success must carry no gate summary: {v}"
        );
        assert_eq!(v["report_path"], ".huskies/merge_reports/51_story_ok-1.log");
    }

    /// AC1/AC2: a gate-failure response carries a condensed summary (failing
    /// gate + parsed test names + tail), plus the full-report path — not the
    /// raw multi-KB output.
    #[test]
    fn tool_get_merge_status_completed_gate_failure_has_condensed_summary() {
        crate::crdt_state::init_for_test();
        let output = "running 1 test\ntest foo::bar ... FAILED\n\n\
             failures:\n    foo::bar\n\ntest result: FAILED. 0 passed; 1 failed\n"
            .to_string();
        let report = crate::agents::merge::MergeReport {
            story_id: "52_story_fail".to_string(),
            result: crate::agents::merge::MergeResult::GateFailure {
                output,
                failure_kind: Some(crate::agents::gates::GateFailureKind::Test),
            },
            worktree_cleaned_up: false,
            story_archived: false,
            report_path: Some(".huskies/merge_reports/52_story_fail-1.log".to_string()),
        };
        let report_json = serde_json::to_string(&report).unwrap();
        crate::crdt_state::write_merge_job(
            "52_story_fail",
            "completed",
            1.0,
            Some(2.0),
            Some(&report_json),
        );

        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_get_merge_status(&json!({"story_id": "52_story_fail"}), &ctx).unwrap();
        let v: serde_json::Value = serde_json::from_str(&result).unwrap();
        let summary = v["gate_summary"]
            .as_str()
            .expect("gate_summary must be present on failure");
        assert!(
            summary.contains("Failing gate: tests"),
            "summary: {summary}"
        );
        assert!(
            summary.contains("Failing tests: foo::bar"),
            "summary: {summary}"
        );
        assert_eq!(
            v["report_path"],
            ".huskies/merge_reports/52_story_fail-1.log"
        );
    }

    // 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"));
    }

    #[tokio::test]
    async 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).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("story_id"));
    }

    #[tokio::test]
    async 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).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("reason"));
    }

    #[tokio::test]
    async 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,
        )
        .await;
        assert!(result.is_ok());
        let msg = result.unwrap();
        assert!(msg.contains("42_story_foo"));
        assert!(msg.contains("Unresolvable merge conflicts"));
    }
}
