//! MCP status tools — pipeline status, story triage, and acceptance criteria inspection.
use crate::http::context::AppContext;
use serde_json::{Value, json};
use std::fs;
use std::path::{Path, PathBuf};

/// Parse all AC items from a story file, returning (text, is_checked) pairs.
fn parse_ac_items(contents: &str) -> Vec<(String, bool)> {
    let mut in_ac_section = false;
    let mut items = Vec::new();

    for line in contents.lines() {
        let trimmed = line.trim();
        if trimmed == "## Acceptance Criteria" {
            in_ac_section = true;
            continue;
        }
        // Stop at the next heading
        if in_ac_section && trimmed.starts_with("## ") {
            break;
        }
        if in_ac_section {
            if let Some(rest) = trimmed
                .strip_prefix("- [x] ")
                .or(trimmed.strip_prefix("- [X] "))
            {
                items.push((rest.to_string(), true));
            } else if let Some(rest) = trimmed.strip_prefix("- [ ] ") {
                items.push((rest.to_string(), false));
            }
        }
    }

    items
}

/// Find the most recent log file for any agent under `.huskies/logs/{story_id}/`.
fn find_most_recent_log(project_root: &Path, story_id: &str) -> Option<PathBuf> {
    let dir = project_root.join(".huskies").join("logs").join(story_id);

    if !dir.is_dir() {
        return None;
    }

    let mut best: Option<(PathBuf, std::time::SystemTime)> = None;

    let entries = fs::read_dir(&dir).ok()?;
    for entry in entries.flatten() {
        let path = entry.path();
        let name = match path.file_name().and_then(|n| n.to_str()) {
            Some(n) => n.to_string(),
            None => continue,
        };
        if !name.ends_with(".log") {
            continue;
        }
        let modified = match entry.metadata().and_then(|m| m.modified()) {
            Ok(t) => t,
            Err(_) => continue,
        };
        if best.as_ref().is_none_or(|(_, t)| modified > *t) {
            best = Some((path, modified));
        }
    }

    best.map(|(p, _)| p)
}

/// Return the last N raw lines from a file.
fn last_n_lines(path: &Path, n: usize) -> Result<Vec<String>, String> {
    let content = fs::read_to_string(path).map_err(|e| format!("Failed to read log file: {e}"))?;
    let lines: Vec<String> = content
        .lines()
        .rev()
        .take(n)
        .map(|l| l.to_string())
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect();
    Ok(lines)
}

/// Run `git diff --stat {base}...HEAD` in the worktree.
async fn git_diff_stat(worktree: &Path, base: &str) -> Option<String> {
    let dir = worktree.to_path_buf();
    let base_arg = format!("{base}...HEAD");
    tokio::task::spawn_blocking(move || {
        let output = std::process::Command::new("git")
            .args(["diff", "--stat", &base_arg])
            .current_dir(&dir)
            .output()
            .ok()?;
        if output.status.success() {
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        } else {
            None
        }
    })
    .await
    .ok()
    .flatten()
}

/// Return the last N commit messages on the current branch relative to base.
async fn git_log_commits(worktree: &Path, base: &str, count: usize) -> Option<Vec<String>> {
    let dir = worktree.to_path_buf();
    let range = format!("{base}..HEAD");
    let count_str = count.to_string();
    tokio::task::spawn_blocking(move || {
        let output = std::process::Command::new("git")
            .args(["log", &range, "--oneline", &format!("-{count_str}")])
            .current_dir(&dir)
            .output()
            .ok()?;
        if output.status.success() {
            let lines: Vec<String> = String::from_utf8(output.stdout)
                .ok()?
                .lines()
                .filter(|l| !l.is_empty())
                .map(|l| l.to_string())
                .collect();
            Some(lines)
        } else {
            None
        }
    })
    .await
    .ok()
    .flatten()
}

/// Return the active branch name for the given directory.
async fn git_branch(dir: &Path) -> Option<String> {
    let dir = dir.to_path_buf();
    tokio::task::spawn_blocking(move || {
        let output = std::process::Command::new("git")
            .args(["rev-parse", "--abbrev-ref", "HEAD"])
            .current_dir(&dir)
            .output()
            .ok()?;
        if output.status.success() {
            Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
        } else {
            None
        }
    })
    .await
    .ok()
    .flatten()
}

/// MCP tool: show full details for a work item in any pipeline stage.
pub(super) async fn tool_show(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let raw_id = args
        .get("story_id")
        .and_then(|v| v.as_str())
        .ok_or("Missing required argument: story_id")?;

    let root = ctx.state.get_project_root()?;

    // Resolve numeric prefix (e.g. "5") to full story_id via find_story_by_number.
    let (story_id, contents) = if raw_id.chars().all(|c| c.is_ascii_digit()) {
        let (sid, _, _, content) = crate::chat::lookup::find_story_by_number(&root, raw_id)
            .ok_or_else(|| {
                format!("No work item with number '{raw_id}' found in any pipeline stage.")
            })?;
        let body = content.ok_or_else(|| {
            format!("Work item '{sid}' found in pipeline but its content is unavailable.")
        })?;
        (sid, body)
    } else {
        let body = crate::db::read_content(crate::db::ContentKey::Story(raw_id))
            .ok_or_else(|| format!("Work item '{raw_id}' not found in any pipeline stage."))?;
        (raw_id.to_string(), body)
    };

    let story_id = story_id.as_str();

    let typed_item = crate::pipeline_state::read_typed(story_id)
        .map_err(|e| format!("Failed to read pipeline state: {e}"))?;

    // --- Metadata (story 929: CRDT-first, yaml_residue marks gaps) ---
    let mut front_matter = serde_json::Map::new();
    if let Some(view) = crate::crdt_state::read_item(story_id) {
        front_matter.insert("name".to_string(), json!(view.name()));
        front_matter.insert("stage".to_string(), json!(view.stage().dir_name()));
        if let Some(agent) = view.agent() {
            front_matter.insert("agent".to_string(), json!(agent));
        }
        if let Some(qa) = view.qa_mode() {
            front_matter.insert("qa".to_string(), json!(qa.as_str()));
        }
        let rc = view.retry_count();
        if rc > 0 {
            front_matter.insert("retry_count".to_string(), json!(rc));
        }
        let deps = view.depends_on();
        if !deps.is_empty() {
            front_matter.insert("depends_on".to_string(), json!(deps));
        }
        let origin_str = view.origin().unwrap_or("unknown");
        front_matter.insert("origin".to_string(), json!(origin_str));
        let stage_claim = typed_item.as_ref().and_then(|t| match &t.stage {
            crate::pipeline_state::Stage::Coding { claim, .. } => claim.as_ref(),
            crate::pipeline_state::Stage::Merge { claim, .. } => claim.as_ref(),
            _ => None,
        });
        if let Some(claim) = stage_claim {
            front_matter.insert("claimed_by".to_string(), json!(claim.agent.0.as_str()));
            front_matter.insert(
                "claimed_at".to_string(),
                json!(claim.claimed_at.timestamp() as f64),
            );
        }
    }

    if let Some(job) = crate::crdt_state::read_merge_job(story_id)
        && let Some(mf) = job.error
    {
        front_matter.insert("merge_failure".to_string(), json!(mf));
    }

    // --- AC checklist ---
    let ac_items: Vec<Value> = parse_ac_items(&contents)
        .into_iter()
        .map(|(text, checked)| json!({ "text": text, "checked": checked }))
        .collect();

    // --- Worktree ---
    let worktree_path = root.join(".huskies").join("worktrees").join(story_id);
    let (_, worktree_info) = if worktree_path.is_dir() {
        let branch = git_branch(&worktree_path).await;
        (
            branch.clone(),
            Some(json!({
                "path": worktree_path.to_string_lossy(),
                "branch": branch,
            })),
        )
    } else {
        (None, None)
    };

    // --- Git diff stat ---
    let diff_stat = if worktree_path.is_dir() {
        git_diff_stat(&worktree_path, "master").await
    } else {
        None
    };

    // --- Last 5 commits ---
    let commits = if worktree_path.is_dir() {
        git_log_commits(&worktree_path, "master", 5).await
    } else {
        None
    };

    // --- Most recent agent log (last 20 lines) ---
    let agent_log = match find_most_recent_log(&root, story_id) {
        Some(log_path) => {
            let filename = log_path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("")
                .to_string();
            match last_n_lines(&log_path, 20) {
                Ok(lines) => Some(json!({
                    "file": filename,
                    "lines": lines,
                })),
                Err(_) => None,
            }
        }
        None => None,
    };

    let result = json!({
        "story_id": story_id,
        "front_matter": front_matter,
        "acceptance_criteria": ac_items,
        "worktree": worktree_info,
        "git_diff_stat": diff_stat,
        "commits": commits,
        "agent_log": agent_log,
    });

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

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn parse_ac_items_returns_checked_and_unchecked() {
        let content = "---\nname: test\n---\n\n## Acceptance Criteria\n\n- [ ] item one\n- [x] item two\n- [X] item three\n\n## Out of Scope\n\n- [ ] not an ac\n";
        let items = parse_ac_items(content);
        assert_eq!(items.len(), 3);
        assert_eq!(items[0], ("item one".to_string(), false));
        assert_eq!(items[1], ("item two".to_string(), true));
        assert_eq!(items[2], ("item three".to_string(), true));
    }

    #[test]
    fn parse_ac_items_empty_when_no_section() {
        let content = "---\nname: test\n---\n\nNo AC section here.\n";
        let items = parse_ac_items(content);
        assert!(items.is_empty());
    }

    #[test]
    fn find_most_recent_log_returns_none_for_missing_dir() {
        let tmp = tempdir().unwrap();
        let result = find_most_recent_log(tmp.path(), "nonexistent_story");
        assert!(result.is_none());
    }

    #[test]
    fn find_most_recent_log_returns_newest_file() {
        let tmp = tempdir().unwrap();
        let log_dir = tmp
            .path()
            .join(".huskies")
            .join("logs")
            .join("42_story_foo");
        fs::create_dir_all(&log_dir).unwrap();

        let old_path = log_dir.join("coder-1-sess-old.log");
        fs::write(&old_path, "old content").unwrap();

        // Ensure different mtime
        std::thread::sleep(std::time::Duration::from_millis(50));

        let new_path = log_dir.join("coder-1-sess-new.log");
        fs::write(&new_path, "new content").unwrap();

        let result = find_most_recent_log(tmp.path(), "42_story_foo").unwrap();
        assert!(
            result.to_string_lossy().contains("sess-new"),
            "Expected newest file, got: {}",
            result.display()
        );
    }

    #[tokio::test]
    async fn tool_show_returns_error_for_missing_story() {
        let tmp = tempdir().unwrap();
        let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
        let result = tool_show(&json!({"story_id": "999_story_nonexistent"}), &ctx).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("not found"));
    }

    #[tokio::test]
    async fn tool_show_returns_retry_count_and_depends_on() {
        let tmp = tempdir().unwrap();

        crate::crdt_state::init_for_test();
        crate::db::ensure_content_store();
        let story_content = "# Story\n\n## Acceptance Criteria\n\n- [ ] Do the thing\n";
        crate::db::write_item_with_content(
            "9887_story_blocked_test",
            "2_current",
            story_content,
            crate::db::ItemMeta::named("Blocked Story"),
        );
        crate::crdt_state::set_retry_count("9887_story_blocked_test", 3);
        crate::crdt_state::set_depends_on("9887_story_blocked_test", &[100, 200]);

        let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
        let result = tool_show(&json!({"story_id": "9887_story_blocked_test"}), &ctx)
            .await
            .unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();

        assert_eq!(parsed["front_matter"]["retry_count"], 3);
        let depends_on = parsed["front_matter"]["depends_on"].as_array().unwrap();
        assert_eq!(depends_on.len(), 2);
        assert_eq!(depends_on[0], 100);
        assert_eq!(depends_on[1], 200);
    }

    #[tokio::test]
    async fn tool_show_returns_story_data() {
        let tmp = tempdir().unwrap();

        crate::db::ensure_content_store();
        let story_content = "# Story\n\n## Acceptance Criteria\n\n- [ ] First criterion\n- [x] Second criterion\n\n## Out of Scope\n\n- nothing\n";
        crate::db::write_item_with_content(
            "9886_story_status_test",
            "2_current",
            story_content,
            crate::db::ItemMeta::named("My Test Story"),
        );
        crate::crdt_state::set_agent(
            "9886_story_status_test",
            Some(crate::config::AgentName::Coder1),
        );

        let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
        let result = tool_show(&json!({"story_id": "9886_story_status_test"}), &ctx)
            .await
            .unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();

        assert_eq!(parsed["story_id"], "9886_story_status_test");
        assert_eq!(parsed["front_matter"]["name"], "My Test Story");
        assert_eq!(parsed["front_matter"]["agent"], "coder-1");

        let ac = parsed["acceptance_criteria"].as_array().unwrap();
        assert_eq!(ac.len(), 2);
        assert_eq!(ac[0]["text"], "First criterion");
        assert_eq!(ac[0]["checked"], false);
        assert_eq!(ac[1]["text"], "Second criterion");
        assert_eq!(ac[1]["checked"], true);
    }
}
