//! MCP agent lifecycle tools — start, stop, wait, and list agents.

use serde_json::{Value, json};

use crate::http::context::AppContext;

use super::worktree::get_worktree_commits;

pub(crate) async fn tool_start_agent(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());

    let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
    // When an explicit agent_name is provided, persist the agent pin in the
    // CRDT register before starting — same path as the Matrix `assign` command.
    let info = if let Some(name) = agent_name {
        crate::service::work_item::assign_and_start(
            story_id,
            name,
            &project_root,
            &ctx.services.agents,
        )
        .await?
    } else {
        ctx.services
            .agents
            .start_agent(&project_root, story_id, None, None, None)
            .await?
    };

    // Story 929: the legacy "snapshot coverage baseline into story YAML"
    // hook is gone — the field was write-only (nothing read it back) and
    // the per-run report at `.huskies/coverage/server.json` is the
    // authoritative source for downstream tools.

    serde_json::to_string_pretty(&json!({
        "story_id": info.story_id,
        "agent_name": info.agent_name,
        "status": info.status.to_string(),
        "session_id": info.session_id,
        "worktree_path": info.worktree_path,
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

pub(crate) async fn tool_stop_agent(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())
        .ok_or("Missing required argument: agent_name")?;

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

    Ok(format!(
        "Agent '{agent_name}' for story '{story_id}' stopped."
    ))
}

/// List all agents with their current status, filtered to non-archived stories.
pub(crate) async fn tool_list_agents(ctx: &AppContext) -> Result<String, String> {
    let project_root = ctx.services.agents.get_project_root(&ctx.state).ok();
    let agents = ctx.services.agents.list_agents().await?;
    let mut entries: Vec<serde_json::Value> = agents
        .iter()
        .filter(|a| {
            project_root
                .as_deref()
                .map(|root| !crate::service::agents::is_archived(root, &a.story_id))
                .unwrap_or(true)
        })
        .map(|a| {
            json!({
                "story_id": a.story_id,
                "agent_name": a.agent_name,
                "status": a.status.to_string(),
                "session_id": a.session_id,
                "worktree_path": a.worktree_path,
            })
        })
        .collect();

    // Append a synthetic entry for each deterministic merge holding the merge lock.
    let running_merges = ctx.services.agents.list_running_merges()?;
    for story_id in running_merges {
        entries.push(json!({
            "story_id": story_id,
            "agent_name": "deterministic-merge",
            "status": "Running",
            "session_id": null,
            "worktree_path": null,
        }));
    }

    serde_json::to_string_pretty(&json!(entries)).map_err(|e| format!("Serialization error: {e}"))
}

pub(crate) async fn tool_wait_for_agent(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())
        .ok_or("Missing required argument: agent_name")?;
    let timeout_ms = args
        .get("timeout_ms")
        .and_then(|v| v.as_u64())
        .unwrap_or(300_000); // default: 5 minutes

    let info = ctx
        .services
        .agents
        .wait_for_agent(story_id, agent_name, timeout_ms)
        .await?;

    let commits = match (&info.worktree_path, &info.base_branch) {
        (Some(wt_path), Some(base)) => get_worktree_commits(wt_path, base).await,
        _ => None,
    };

    let completion = info.completion.as_ref().map(|r| {
        json!({
            "summary": r.summary,
            "gates_passed": r.gates_passed,
            "gate_output": r.gate_output,
        })
    });

    serde_json::to_string_pretty(&json!({
        "story_id": info.story_id,
        "agent_name": info.agent_name,
        "status": info.status.to_string(),
        "session_id": info.session_id,
        "worktree_path": info.worktree_path,
        "base_branch": info.base_branch,
        "commits": commits,
        "completion": completion,
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

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

    #[tokio::test]
    async fn tool_list_agents_empty() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_list_agents(&ctx).await.unwrap();
        let parsed: Vec<serde_json::Value> = serde_json::from_str(&result).unwrap();
        assert!(parsed.is_empty());
    }

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

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

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

    #[tokio::test]
    async fn tool_start_agent_no_agent_name_no_coder_returns_clear_error() {
        // Config has only a supervisor — start_agent without agent_name should
        // refuse rather than silently assigning supervisor.
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        std::fs::create_dir_all(&sk).unwrap();
        std::fs::write(
            sk.join("project.toml"),
            r#"
[[agent]]
name = "supervisor"
stage = "other"
"#,
        )
        .unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_start_agent(&json!({"story_id": "42_my_story"}), &ctx).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(
            err.contains("coder"),
            "error should mention 'coder', got: {err}"
        );
    }

    #[tokio::test]
    async fn tool_start_agent_no_agent_name_picks_coder_not_supervisor() {
        // Config has supervisor first, then coder-1. Without agent_name the
        // coder should be selected, not supervisor. The call will fail due to
        // missing git repo / worktree, but the error must NOT be about
        // "No coder agent configured".
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        std::fs::create_dir_all(&sk).unwrap();
        std::fs::write(
            sk.join("project.toml"),
            r#"
[[agent]]
name = "supervisor"
stage = "other"

[[agent]]
name = "coder-1"
stage = "coder"
"#,
        )
        .unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_start_agent(&json!({"story_id": "42_my_story"}), &ctx).await;
        // May succeed or fail for infrastructure reasons (no git repo), but
        // must NOT fail with "No coder agent configured".
        if let Err(err) = result {
            assert!(
                !err.contains("No coder agent configured"),
                "should not fail on agent selection, got: {err}"
            );
            // Should also not complain about supervisor being absent.
            assert!(
                !err.contains("supervisor"),
                "should not select supervisor, got: {err}"
            );
        }
    }

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

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

    #[tokio::test]
    async fn wait_for_agent_tool_nonexistent_agent_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        let result = tool_wait_for_agent(
            &json!({"story_id": "99_nope", "agent_name": "bot", "timeout_ms": 50}),
            &ctx,
        )
        .await;
        // No agent registered — should error
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn wait_for_agent_tool_returns_completed_agent() {
        use crate::agents::AgentStatus;
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());
        ctx.services
            .agents
            .inject_test_agent("41_story", "worker", AgentStatus::Completed);

        let result = tool_wait_for_agent(
            &json!({"story_id": "41_story", "agent_name": "worker"}),
            &ctx,
        )
        .await
        .unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
        assert_eq!(parsed["status"], "completed");
        assert_eq!(parsed["story_id"], "41_story");
        assert_eq!(parsed["agent_name"], "worker");
        // commits key present (may be null since no real worktree)
        assert!(parsed.get("commits").is_some());
        // completion key present (null for agents that didn't call report_completion)
        assert!(parsed.get("completion").is_some());
    }
}
