use crate::model::{CodeGraph, NodeKind};
use crate::query::index::QueryIndex;
use anyhow::Result;
use serde_json::{Value, json};

/// Truncate to max chars with `...` suffix.
fn truncate(s: &str, max: usize) -> String {
    if s.len() <= max {
        return s.to_string();
    }
    let mut end = max;
    while end > 0 && !s.is_char_boundary(end) {
        end -= 1;
    }
    format!("{}...", &s[..end])
}

/// Format a node's range as `start-end` or `start`.
fn format_range(node: &crate::model::Node) -> String {
    match &node.range {
        Some(r) if r.start_line != r.end_line => {
            format!("{}:{}", r.start_line, r.end_line)
        }
        Some(r) => format!("{}", r.start_line),
        None => "?".to_string(),
    }
}

/// Generates a markdown report string. Returns kind:"report" with content.
pub fn report(graph: &CodeGraph) -> Result<Value> {
    let index = QueryIndex::new(graph);

    let file_count = graph
        .nodes
        .iter()
        .filter(|n| n.kind == NodeKind::File)
        .count();

    let mut out = String::new();
    out.push_str("# GRAPH_REPORT\n\n");

    // Stats
    out.push_str("## Stats\n");
    out.push_str(&format!(
        "- Nodes: {}, Edges: {}, Files: {}\n",
        graph.nodes.len(),
        graph.edges.len(),
        file_count,
    ));

    // Top 5 modules by symbol count
    use std::collections::HashMap;
    let mut file_symbol_count: HashMap<&str, usize> = HashMap::new();
    for node in &graph.nodes {
        if node.kind == NodeKind::File || node.kind == NodeKind::Project {
            continue;
        }
        if let Some(file) = &node.file {
            *file_symbol_count.entry(file.as_str()).or_insert(0) += 1;
        }
    }
    let mut top_modules: Vec<(&str, usize)> = file_symbol_count.into_iter().collect();
    top_modules.sort_by(|a, b| b.1.cmp(&a.1));

    out.push_str("\n## Top Modules by Symbol Count\n");
    for (file, count) in top_modules.iter().take(5) {
        out.push_str(&format!("- `{}` symbols={}\n", file, count));
    }

    // Top 10 hot symbols
    let mut nodes_with_deg: Vec<(&crate::model::Node, usize)> = graph
        .nodes
        .iter()
        .map(|n| (n, index.degree(&n.id)))
        .collect();
    nodes_with_deg.sort_by(|a, b| b.1.cmp(&a.1));

    out.push_str("\n## Top 10 Hot Symbols\n");
    for (node, deg) in nodes_with_deg.iter().take(10) {
        let file = node.file.as_deref().unwrap_or("?");
        let range = format_range(node);
        out.push_str(&format!(
            "- {} `{}` in {}:{} degree={}",
            node.kind.as_str(),
            node.qualified_name,
            file,
            range,
            deg,
        ));
        if let Some(sig) = &node.signature {
            out.push_str(&format!("\n  sig: {}", truncate(sig, 120)));
        }
        if let Some(docs) = &node.docs {
            let first_line = docs.lines().next().unwrap_or(docs);
            out.push_str(&format!("\n  docs: {}", truncate(first_line, 120)));
        }
        out.push('\n');
    }

    // Warnings
    if !graph.warnings.is_empty() {
        out.push_str("\n## Warnings\n");
        for w in &graph.warnings {
            out.push_str(&format!("- {}\n", w));
        }
    }

    out.push_str("\n---\nGenerated by pyscope\n");

    Ok(json!({
        "kind": "report",
        "content": out,
    }))
}
