//! Brief command — one-line-per-function summary for a file

use std::collections::HashMap;

use anyhow::{bail, Context as _, Result};

use cqs::rel_display;
use cqs::store::{ChunkSummary, Store};
use std::path::Path;

/// One-line summary entry for a function in the file.
#[derive(Debug, serde::Serialize)]
struct BriefEntry {
    name: String,
    chunk_type: String,
    line_start: u32,
    callers: u64,
    tests: u64,
}

/// Top-level JSON output for the brief command.
#[derive(Debug, serde::Serialize)]
struct BriefOutput {
    file: String,
    functions: Vec<BriefEntry>,
    total: usize,
}

/// Data returned by `build_brief_data`: chunks with caller and test counts.
#[derive(Debug)]
struct BriefData {
    chunks: Vec<ChunkSummary>,
    caller_counts: HashMap<String, u64>,
    test_counts: HashMap<String, u64>,
}

/// Build brief data for a file: load chunks, count callers and test coverage.
fn build_brief_data<Mode>(store: &Store<Mode>, path: &str) -> Result<BriefData> {
    let _span = tracing::info_span!("build_brief_data", path).entered();

    // PB-V1.29-1: normalize backslash input from Windows / agent pipelines.
    // `get_chunks_by_origin` matches on the stored `origin` column which is
    // forward-slash-normalized; unnormalized `src\foo.rs` silently returns empty.
    let normalized = cqs::normalize_path(Path::new(path));
    let chunks = store
        .get_chunks_by_origin(&normalized)
        .context("Failed to load chunks for file")?;
    if chunks.is_empty() {
        bail!(
            "No indexed chunks found for '{}'. Is the file indexed?",
            path
        );
    }

    // Dedup by name — multiple window_idx values produce duplicate entries
    let mut seen = std::collections::HashSet::new();
    let chunks: Vec<ChunkSummary> = chunks
        .into_iter()
        .filter(|c| seen.insert(c.name.clone()))
        .collect();

    let names: Vec<&str> = chunks.iter().map(|c| c.name.as_str()).collect();

    // EH-V1.29-1: propagate query errors so agents can't mistake "query failed"
    // for "genuine zero". Previously these three calls used `.unwrap_or_else`
    // with empty fallbacks, hiding store failures behind a cosmetic warn log.
    let caller_counts = store
        .get_caller_counts_batch(&names)
        .context("Failed to fetch caller counts for brief")?;

    // Get test coverage via call graph BFS (same approach as test_map)
    let graph = store
        .get_call_graph()
        .context("Failed to load call graph for brief")?;
    let test_chunks = store
        .find_test_chunks()
        .context("Failed to find test chunks for brief")?;

    let mut test_counts: HashMap<String, u64> = HashMap::new();
    for chunk in &chunks {
        // Reverse BFS from chunk to find test ancestors (depth 5)
        let mut visited = std::collections::HashSet::new();
        let mut queue = std::collections::VecDeque::new();
        visited.insert(chunk.name.clone());
        queue.push_back((chunk.name.clone(), 0usize));

        while let Some((current, depth)) = queue.pop_front() {
            if depth >= 5 {
                continue;
            }
            if let Some(callers) = graph.reverse.get(current.as_str()) {
                for caller in callers {
                    if visited.insert(caller.to_string()) {
                        queue.push_back((caller.to_string(), depth + 1));
                    }
                }
            }
        }

        let count = test_chunks
            .iter()
            .filter(|t| visited.contains(&t.name) && t.name != chunk.name)
            .count() as u64;
        test_counts.insert(chunk.name.clone(), count);
    }

    Ok(BriefData {
        chunks,
        caller_counts,
        test_counts,
    })
}

pub(crate) fn cmd_brief(
    ctx: &crate::cli::CommandContext<'_, cqs::store::ReadOnly>,
    path: &str,
    json: bool,
) -> Result<()> {
    let _span = tracing::info_span!("cmd_brief", path).entered();
    let store = &ctx.store;
    let root = &ctx.root;

    let data = build_brief_data(store, path)?;
    let rel = rel_display(&std::path::PathBuf::from(path), root);

    let entries: Vec<BriefEntry> = data
        .chunks
        .iter()
        .map(|c| BriefEntry {
            name: c.name.clone(),
            chunk_type: c.chunk_type.to_string(),
            line_start: c.line_start,
            callers: *data.caller_counts.get(&c.name).unwrap_or(&0),
            tests: *data.test_counts.get(&c.name).unwrap_or(&0),
        })
        .collect();

    if json {
        let total = entries.len();
        let result = BriefOutput {
            file: rel.clone(),
            functions: entries,
            total,
        };
        crate::cli::json_envelope::emit_json(&result)?;
    } else {
        use colored::Colorize;
        println!("{} ({})", rel.bold(), entries.len());
        println!(
            "  {:<30} {:<12} {:>7} {:>7}",
            "Name", "Type", "Callers", "Tests"
        );
        println!("  {}", "-".repeat(60));
        for e in &entries {
            println!(
                "  {:<30} {:<12} {:>7} {:>7}",
                e.name, e.chunk_type, e.callers, e.tests
            );
        }
    }

    Ok(())
}

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

    #[test]
    fn brief_entry_serializes_correctly() {
        let entry = BriefEntry {
            name: "my_func".to_string(),
            chunk_type: "Function".to_string(),
            line_start: 10,
            callers: 3,
            tests: 1,
        };
        let json = serde_json::to_value(&entry).unwrap();
        assert_eq!(json["name"], "my_func");
        assert_eq!(json["chunk_type"], "Function");
        assert_eq!(json["line_start"], 10);
        assert_eq!(json["callers"], 3);
        assert_eq!(json["tests"], 1);
    }

    #[test]
    fn brief_output_serialization() {
        let output = BriefOutput {
            file: "src/lib.rs".to_string(),
            functions: vec![
                BriefEntry {
                    name: "foo".to_string(),
                    chunk_type: "Function".to_string(),
                    line_start: 1,
                    callers: 2,
                    tests: 0,
                },
                BriefEntry {
                    name: "bar".to_string(),
                    chunk_type: "Method".to_string(),
                    line_start: 10,
                    callers: 0,
                    tests: 1,
                },
            ],
            total: 2,
        };
        let json = serde_json::to_value(&output).unwrap();
        assert_eq!(json["file"], "src/lib.rs");
        assert_eq!(json["total"], 2);
        assert_eq!(json["functions"][0]["name"], "foo");
        assert_eq!(json["functions"][0]["callers"], 2);
        assert_eq!(json["functions"][1]["callers"], 0);
        assert_eq!(json["functions"][1]["tests"], 1);
    }
}
