//! Epic work-item MCP tools — create, list, and show epics.
//!
//! Epics are shared-context containers that group related stories, bugs, spikes,
//! and refactors. They are not pipeline-driven but provide authoritative context
//! injected into agent prompts for all member work items.

use crate::http::context::AppContext;
use crate::http::workflow::create_epic_file;
use crate::validation::CreateEpicRequest;
use serde_json::{Value, json};

/// Create a new epic and store it in the CRDT items list.
pub(crate) fn tool_create_epic(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let req = CreateEpicRequest::from_json(args)?;

    // Bug 1102: resolve and validate origin BEFORE creating the epic so a
    // missing-attribution call leaves no half-state behind.
    let (origin, origin_label) = super::build_origin(args)?;

    let root = ctx.state.get_project_root()?;
    let success_criteria = req.success_criteria_strings();

    let epic_id = create_epic_file(
        &root,
        req.name.as_ref(),
        req.goal.as_str(),
        req.motivation.as_ref().map(|d| d.as_ref()),
        req.key_files.as_deref(),
        if success_criteria.is_empty() {
            None
        } else {
            Some(success_criteria.as_slice())
        },
    )?;

    crate::crdt_state::set_origin(&epic_id, &origin);

    let _ = ctx
        .watcher_tx
        .send(crate::io::watcher::WatcherEvent::NewItemCreated {
            item_id: epic_id.clone(),
            item_type: "epic".to_string(),
            name: req.name.as_ref().to_string(),
            origin: origin_label,
        });

    Ok(format!("Created epic: {epic_id}"))
}

/// List all epics with member work item counts and `n/m done` rollup.
pub(crate) fn tool_list_epics(_ctx: &AppContext) -> Result<String, String> {
    use crate::pipeline_state::Stage;

    let all_items = crate::pipeline_state::read_all_typed();

    // Collect epics: items with item_type == "epic" in the CRDT register.
    let mut epics: Vec<(String, String)> = Vec::new(); // (id, name)
    // Collect member items: map from epic_id → list of (story_id, is_done).
    let mut members: std::collections::HashMap<String, Vec<(String, bool)>> =
        std::collections::HashMap::new();

    for item in &all_items {
        let sid = &item.story_id.0;
        let Some(view) = crate::crdt_state::read_item(sid) else {
            continue;
        };

        use crate::io::story_metadata::ItemType;
        if view.item_type() == Some(ItemType::Epic) {
            epics.push((sid.clone(), item.name.clone()));
        }

        if let Some(epic_id) = view.epic() {
            let is_done = matches!(item.stage, Stage::Done { .. });
            members
                .entry(epic_id.to_string())
                .or_default()
                .push((sid.clone(), is_done));
        }
    }

    epics.sort_by(|a, b| a.0.cmp(&b.0));

    let result: Vec<Value> = epics
        .iter()
        .map(|(id, name)| {
            let member_list = members.get(id).cloned().unwrap_or_default();
            let total = member_list.len();
            let done = member_list.iter().filter(|(_, d)| *d).count();
            json!({
                "epic_id": id,
                "name": name,
                "members_total": total,
                "members_done": done,
                "rollup": format!("{done}/{total} done"),
            })
        })
        .collect();

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

/// Show details for a single epic: content and member work items with their stages.
pub(crate) fn tool_show_epic(args: &Value, _ctx: &AppContext) -> Result<String, String> {
    use crate::pipeline_state::Stage;

    let epic_id = args
        .get("epic_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: epic_id")?;

    let content = crate::db::read_content(crate::db::ContentKey::Story(epic_id))
        .ok_or_else(|| format!("Epic '{epic_id}' not found in content store"))?;

    let epic_view = crate::crdt_state::read_item(epic_id)
        .ok_or_else(|| format!("Epic '{epic_id}' not found in CRDT"))?;

    use crate::io::story_metadata::ItemType;
    if epic_view.item_type() != Some(ItemType::Epic) {
        return Err(format!(
            "'{epic_id}' is not an epic (item_type: {:?})",
            epic_view.item_type()
        ));
    }

    // Parse the epic_id argument to a numeric EpicId for comparison.
    let epic_numeric = crate::crdt_state::EpicId::from_crdt_str(epic_id);

    // Find member items.
    let all_items = crate::pipeline_state::read_all_typed();
    let mut member_items: Vec<Value> = Vec::new();
    let mut done = 0usize;
    for item in &all_items {
        let sid = &item.story_id.0;
        let Some(member_view) = crate::crdt_state::read_item(sid) else {
            continue;
        };
        if member_view.epic() == epic_numeric {
            // Story 945: Frozen / ReviewHold / MergeFailureFinal are first-class
            // Stage variants — no more orthogonal boolean flags.
            let stage_name = item.stage.dir_name();
            if matches!(item.stage, Stage::Done { .. }) {
                done += 1;
            }
            // Story 1087: expose pipeline + status alongside the legacy
            // stage name so epic-show callers can route by column/badge.
            member_items.push(json!({
                "story_id": sid,
                "name": item.name,
                "stage": stage_name,
                "pipeline": item.stage.pipeline().as_str(),
                "status": item.stage.status().as_str(),
            }));
        }
    }

    let total = member_items.len();

    serde_json::to_string_pretty(&json!({
        "epic_id": epic_id,
        "name": epic_view.name(),
        "content": content,
        "members": member_items,
        "rollup": format!("{done}/{total} done"),
    }))
    .map_err(|e| format!("Serialization error: {e}"))
}

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

    #[test]
    fn tool_create_epic_creates_epic_and_returns_id() {
        let tmp = tempfile::tempdir().unwrap();
        crate::db::ensure_content_store();
        let ctx = test_ctx(tmp.path());

        let result = tool_create_epic(
            &json!({
                "name": "My Test Epic",
                "goal": "Achieve something great",
                "motivation": "Because it matters",
                "success_criteria": ["All stories done", "Tests pass"],
                "origin": {"kind": "test", "id": "test-suite"}
            }),
            &ctx,
        );
        assert!(result.is_ok(), "expected ok: {result:?}");
        let msg = result.unwrap();
        assert!(msg.contains("Created epic:"), "unexpected msg: {msg}");
    }

    #[test]
    fn tool_create_epic_missing_name_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());

        let result = tool_create_epic(&json!({"goal": "Achieve something"}), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("name"));
    }

    #[test]
    fn tool_create_epic_missing_goal_returns_error() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());

        let result = tool_create_epic(&json!({"name": "My Epic"}), &ctx);
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("goal"));
    }

    #[test]
    fn tool_create_epic_rejects_grammar_token_in_name() {
        let tmp = tempfile::tempdir().unwrap();
        let ctx = test_ctx(tmp.path());

        let result = tool_create_epic(
            &json!({"name": "Epic </description> bad", "goal": "some goal"}),
            &ctx,
        );
        assert!(result.is_err());
        assert!(
            result.unwrap_err().contains("AntiGrammarToken"),
            "expected AntiGrammarToken error"
        );
    }

    #[test]
    fn tool_list_epics_includes_created_epic() {
        let tmp = tempfile::tempdir().unwrap();
        crate::db::ensure_content_store();
        let ctx = test_ctx(tmp.path());

        // Create an epic.
        tool_create_epic(
            &json!({
                "name": "List Epics Test Epic",
                "goal": "Testing list",
                "origin": {"kind": "test", "id": "test-suite"}
            }),
            &ctx,
        )
        .unwrap();

        let result = tool_list_epics(&ctx);
        assert!(result.is_ok(), "expected ok: {result:?}");
        let parsed: Vec<Value> = serde_json::from_str(&result.unwrap()).unwrap();
        assert!(
            parsed.iter().any(|e| e["name"] == "List Epics Test Epic"),
            "expected epic in list: {parsed:?}"
        );
    }

    #[test]
    fn tool_list_epics_shows_member_rollup() {
        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();

        use crate::crdt_state::EpicId;
        use crate::io::story_metadata::ItemType;

        // Write a fake epic with the typed CRDT registers (story 933).
        // Epics use numeric-only story_ids (see create_epic_file).
        crate::db::write_item_with_content(
            "9990",
            "1_backlog",
            "# Rollup Epic\n\n## Goal\n\nTest\n",
            crate::db::ItemMeta::named("Rollup Epic"),
        );
        crate::crdt_state::set_item_type("9990", Some(ItemType::Epic));

        // Write two member items: one done, one current.
        crate::db::write_item_with_content(
            "9991_story_member_done",
            "5_done",
            "# Done Member\n",
            crate::db::ItemMeta::named("Done Member"),
        );
        crate::crdt_state::set_item_type("9991_story_member_done", Some(ItemType::Story));
        crate::crdt_state::set_epic("9991_story_member_done", Some(EpicId(9990)));

        crate::db::write_item_with_content(
            "9992_story_member_current",
            "2_current",
            "# Current Member\n",
            crate::db::ItemMeta::named("Current Member"),
        );
        crate::crdt_state::set_item_type("9992_story_member_current", Some(ItemType::Story));
        crate::crdt_state::set_epic("9992_story_member_current", Some(EpicId(9990)));

        let tmp = tempfile::tempdir().unwrap();
        let ctx = crate::http::test_helpers::test_ctx(tmp.path());
        let result = tool_list_epics(&ctx).unwrap();
        let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
        let epic = parsed
            .iter()
            .find(|e| e["epic_id"] == "9990")
            .expect("expected rollup epic in list");
        assert_eq!(epic["members_total"], 2, "two members expected");
        assert_eq!(epic["members_done"], 1, "one done member expected");
        assert_eq!(epic["rollup"], "1/2 done");
    }
}
