use std::collections::HashSet;
use std::path::Path;

use eidos_kernel::schema::relation::{CONTAINS, IMPLEMENTS};
use eidos_kernel::schema::{Edge, Kind, Node, Span};
use tree_sitter::{Node as TsNode, Parser};

use crate::glyph::graph::{FileGraph, ImportMap, RefName};

use super::common::{
    cap_summary, mod_path, module_summary, module_title, paren_contents, span_evidence,
};
use super::imports::{collect_rust_imports, import_ref};
use super::intent::{generic_code_intent_aliases, identifier_words};

pub(crate) fn symbols_in_file(path: &Path, base: &Path) -> FileGraph {
    let mut fg = FileGraph::default();
    let Ok(src) = std::fs::read_to_string(path) else {
        return fg;
    };
    let mut parser = Parser::new();
    if parser
        .set_language(&tree_sitter_rust::LANGUAGE.into())
        .is_err()
    {
        return fg;
    }
    let Some(tree) = parser.parse(src.as_bytes(), None) else {
        return fg;
    };
    fg.parse_error = tree.root_node().has_error();

    let path_str = path.to_string_lossy().into_owned();
    let modpath = mod_path(path, base);
    let title = module_title(path);
    let summary = module_summary(inner_doc(&src), "rust", &title, &modpath);

    // The file itself is a module node, spanning the whole file, and the top-level container.
    let last_line = src.lines().count().max(1);
    let file_mod_id = format!("mod.{modpath}");
    fg.nodes.push(Node {
        id: file_mod_id.clone(),
        kind: Kind::Module,
        subkind: None,
        title: title.clone(),
        summary,
        aliases: module_aliases(&title, &modpath),
        tags: Vec::new(),
        query_examples: Vec::new(),
        source_files: vec![path_str.clone()],
        partition: None,
        span: Some(Span {
            path: path_str.clone(),
            start_line: 1,
            end_line: last_line,
        }),
    });

    // Collect `use` imports once per file so cross-module references can band ImportAware
    // (earned Resolved) instead of GlobalUnique (a coincidental-name guess).
    let imports = collect_rust_imports(tree.root_node(), &src);

    walk(
        tree.root_node(),
        &src,
        &path_str,
        &modpath,
        &file_mod_id,
        None,
        &imports,
        &mut fg,
    );
    fg
}

/// Recursively visit the CST. `container_id` is the node that lexically holds whatever we emit
/// here; descending into an emitted item makes it the container for its own children, and an
/// `impl` block makes its self-type the container for its methods.
#[allow(clippy::too_many_arguments)]
fn walk(
    node: TsNode,
    src: &str,
    path: &str,
    modpath: &str,
    container_id: &str,
    type_qual: Option<&str>,
    imports: &ImportMap,
    fg: &mut FileGraph,
) {
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        match child.kind() {
            // An impl block is not a node: methods inside it are contained by the self-type, and a
            // trait impl adds an `implements` edge. Descend with the type as both container AND id
            // qualifier, so a method's id is `fn.mod::Type::method` — no cross-impl collision.
            "impl_item" => {
                if let Some(type_name) = field_type_name(child, "type", src) {
                    let type_id = format!("type.{modpath}::{type_name}");
                    // A trait impl qualifies its methods by `Type::Trait` (two traits can give a type
                    // same-named methods); an inherent impl by `Type` alone. Keeps method ids unique.
                    let qual = match field_type_name(child, "trait", src) {
                        Some(trait_name) => {
                            fg.edges.push(Edge {
                                from: type_id.clone(),
                                to: format!("trait.{trait_name}"),
                                relation: IMPLEMENTS.into(),
                                evidence: span_evidence(path, child),
                                // Syntax (impl block) — earned Resolved.
                                basis: eidos_kernel::schema::EdgeBasis::Resolved,
                                ..Default::default()
                            });
                            format!("{type_name}::{trait_name}")
                        }
                        None => type_name.clone(),
                    };
                    walk(
                        child,
                        src,
                        path,
                        modpath,
                        &type_id,
                        Some(&qual),
                        imports,
                        fg,
                    );
                } else {
                    // Exotic self-type with no `type_identifier` to qualify by (`impl T for &[u8]`,
                    // primitives, tuples — common in nom/serde). No type node to contain the methods,
                    // so keep the current container but qualify by the impl's LOCATION, so methods
                    // from different exotic impls don't collide on `fn.mod::method`.
                    let qual = format!("impl_{}", child.start_position().row + 1);
                    walk(
                        child,
                        src,
                        path,
                        modpath,
                        container_id,
                        Some(&qual),
                        imports,
                        fg,
                    );
                }
            }
            kind if is_emitted_item(child) => {
                let (k, prefix) = item_kind(kind).unwrap();
                let name_node = child.child_by_field_name("name").unwrap();
                let name = &src[name_node.byte_range()];
                // Any item declared inside an impl/trait carries that qualifier (methods AND
                // associated types like `type Error` / `type Value`, which dozens of impls share);
                // module/top-level items use the module path alone.
                let id = match type_qual {
                    Some(tq) => format!("{prefix}.{modpath}::{tq}::{name}"),
                    None => format!("{prefix}.{modpath}::{name}"),
                };
                fg.nodes.push(Node {
                    id: id.clone(),
                    kind: k,
                    subkind: None,
                    title: name.to_string(),
                    summary: {
                        let doc = doc_comment_above(child, src);
                        if !doc.is_empty() {
                            doc
                        } else {
                            signature_summary(child, src)
                        }
                    },
                    aliases: structural_aliases(child, kind, src, modpath),
                    tags: Vec::new(),
                    query_examples: Vec::new(),
                    source_files: vec![path.to_string()],
                    partition: None,
                    span: Some(Span {
                        path: path.to_string(),
                        start_line: child.start_position().row + 1,
                        end_line: child.end_position().row + 1,
                    }),
                });
                fg.edges.push(Edge {
                    from: container_id.to_string(),
                    to: id.clone(),
                    relation: CONTAINS.into(),
                    evidence: span_evidence(path, child),
                    // Syntax (module/impl containment) — earned Resolved.
                    basis: eidos_kernel::schema::EdgeBasis::Resolved,
                    ..Default::default()
                });
                // Reference sources are symbols with bodies — modules are too generic to attribute
                // calls to. Collect the distinct identifiers in this item's subtree for phase 2.
                if matches!(k, Kind::Function | Kind::Type | Kind::Trait) {
                    let mut idents = Vec::new();
                    collect_idents(child, src, imports, &mut idents, &mut HashSet::new());
                    fg.refs.push((id.clone(), idents));
                }
                // Recurse into CONTAINERS (trait/type/module) to emit their members, qualifying a
                // trait's declarations by the trait. Do NOT descend into a function body: items
                // defined inside a fn (local helper structs/fns) are implementation detail, not API
                // surface — emitting them adds noise and id collisions. (Refs still scan the body.)
                if k != Kind::Function {
                    let child_qual = if k == Kind::Trait { Some(name) } else { None };
                    walk(child, src, path, modpath, &id, child_qual, imports, fg);
                }
            }
            _ => walk(
                child,
                src,
                path,
                modpath,
                container_id,
                type_qual,
                imports,
                fg,
            ),
        }
    }
}

/// True if this node is a declared item we emit (and not a body-less `mod foo;` re-export, which
/// just points at a file-module node we already emit).
fn is_emitted_item(node: TsNode) -> bool {
    if item_kind(node.kind()).is_none() || node.child_by_field_name("name").is_none() {
        return false;
    }
    !(node.kind() == "mod_item" && node.child_by_field_name("body").is_none())
}

/// A file's leading `//!` inner-doc block, joined into one line — the authored MODULE summary (the
/// code analogue of a doc node's `summary`). Stops at the first line that isn't `//!` (after any
/// leading blank lines), so it captures the header and nothing below it.
fn inner_doc(src: &str) -> String {
    // First non-empty `//!` line only — mirrors the docs builder's `first_body_line` summary. The
    // full header over-matches: a verbose module's common vocabulary turns it into a magnet for
    // unrelated queries (measured: indexing the whole header dropped p@1).
    for line in src.lines() {
        let trimmed = line.trim_start();
        let Some(rest) = trimmed.strip_prefix("//!") else {
            if trimmed.is_empty() {
                continue;
            }
            break;
        };
        if !rest.trim().is_empty() {
            return rest.trim().to_string();
        }
    }
    String::new()
}

/// The leading `///` / `//!` doc-comment immediately above an item, joined into one line — the
/// code analogue of a doc node's authored `summary` and the richest retrieval signal a symbol
/// carries. Walks up past attributes (`#[derive(...)]`) and consecutive doc lines; any other node
/// (blank gap, ordinary `//` comment, code) ends the block.
fn doc_comment_above(item: TsNode, src: &str) -> String {
    let mut lines: Vec<String> = Vec::new();
    let mut sib = item.prev_sibling();
    while let Some(s) = sib {
        match s.kind() {
            "attribute_item" => sib = s.prev_sibling(),
            "line_comment" => {
                let text = src[s.byte_range()].trim();
                let Some(rest) = text
                    .strip_prefix("///")
                    .or_else(|| text.strip_prefix("//!"))
                else {
                    break;
                };
                lines.push(rest.trim().to_string());
                sib = s.prev_sibling();
            }
            _ => break,
        }
    }
    lines.reverse();
    lines.join(" ")
}

/// Derive a one-line semantic summary from an item's signature — the fallback when it carries no
/// `///` doc comment. Undocumented code is the dominant cause of `[fallback]` grounding: an empty
/// summary gives BM25F no curated identity surface. The signature line (name, params, return type,
/// kind keyword) is exactly that surface. A real doc comment always wins; this only fills the void.
fn signature_summary(item: TsNode, src: &str) -> String {
    let first_line = src[item.byte_range()].lines().next().unwrap_or("").trim();

    if first_line.contains("fn ") {
        let after_fn = first_line.split("fn ").last().unwrap_or("");
        let name = after_fn.split(['(', '<']).next().unwrap_or("").trim();
        let param_names: Vec<&str> = paren_contents(first_line)
            .split(',')
            .map(|p| {
                p.split(':')
                    .next()
                    .unwrap_or("")
                    .trim()
                    .trim_start_matches("mut ")
                    .trim()
            })
            .filter(|p| !p.is_empty() && *p != "self" && *p != "&self" && *p != "&mut self")
            .collect();
        let ret = first_line
            .contains("->")
            .then(|| {
                // Everything after `->`, up to the body brace / where-clause if both share a line.
                first_line
                    .split("->")
                    .last()
                    .unwrap_or("")
                    .split('{')
                    .next()
                    .unwrap_or("")
                    .trim()
            })
            .filter(|r| !r.is_empty());
        if !param_names.is_empty() {
            return cap_summary(match ret {
                Some(r) => format!("{name} — accepts: {}. Returns: {r}", param_names.join(", ")),
                None => format!("{name} — accepts: {}", param_names.join(", ")),
            });
        }
        return cap_summary(match ret {
            Some(r) => format!("{name} — returns: {r}"),
            None => name.to_string(),
        });
    }

    for (kw, label) in [
        ("struct ", "struct"),
        ("enum ", "enum"),
        ("trait ", "trait"),
    ] {
        if let Some(pos) = first_line.find(kw) {
            // Guard against matching a keyword substring inside a longer word.
            let preceded_ok = pos == 0 || first_line[..pos].ends_with(' ');
            if preceded_ok {
                let rest = &first_line[pos + kw.len()..];
                let name = rest
                    .split(['{', '<', '(', ';', ':'])
                    .next()
                    .unwrap_or("")
                    .trim();
                if !name.is_empty() {
                    return cap_summary(format!("{name} — {label}"));
                }
            }
        }
    }

    cap_summary(first_line.to_string())
}

/// Collect the distinct identifier-ish tokens in a subtree (DFS), into `out` (insertion order)
/// guarded by `seen`. tree-sitter yields these from code only — comments and string literals are
/// other node kinds, so this is a structural mention, not a naive text grep.
fn collect_idents(
    node: TsNode,
    src: &str,
    imports: &ImportMap,
    out: &mut Vec<RefName>,
    seen: &mut HashSet<String>,
) {
    if matches!(
        node.kind(),
        "identifier" | "type_identifier" | "field_identifier"
    ) {
        let tok = &src[node.byte_range()];
        if seen.insert(tok.to_string()) {
            out.push(import_ref(tok, imports));
        }
    }
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        collect_idents(child, src, imports, out, seen);
    }
}

/// The syntactic keyword a symbol is searchable by ("the Hit struct", "the Foo trait") — an
/// identifier-lane alias, the code analogue of an author's `keywords:`. Field/variant names were
/// measured here too and REJECTED: they over-matched (a `recall`/`noise` field stole a locate
/// query), so only the keyword (low-IDF, disambiguated by the name) is kept.
fn structural_aliases(node: TsNode, ts_kind: &str, src: &str, modpath: &str) -> Vec<String> {
    let keyword = kind_keyword(ts_kind);
    let mut aliases = vec![keyword.to_string()];
    if let Some(name_node) = node.child_by_field_name("name") {
        let name = &src[name_node.byte_range()];
        if let Some(words) = identifier_words(name) {
            aliases.push(words.clone());
            aliases.push(format!("{words} {keyword}"));
        }
        aliases.extend(generic_code_intent_aliases(name, modpath, Some(node), src));
    }
    aliases.sort();
    aliases.dedup();
    aliases
}

fn module_aliases(title: &str, modpath: &str) -> Vec<String> {
    let mut aliases = vec!["module".to_string()];
    if let Some(words) = identifier_words(title) {
        aliases.push(words.clone());
        aliases.push(format!("{words} module"));
    } else if !title.trim().is_empty() {
        aliases.push(format!("{} module", title.to_ascii_lowercase()));
    }
    aliases.extend(generic_code_intent_aliases(title, modpath, None, ""));
    aliases.sort();
    aliases.dedup();
    aliases
}

/// The Rust keyword a developer would search a symbol by ("the Hit struct", "the Foo trait").
fn kind_keyword(ts_kind: &str) -> &'static str {
    match ts_kind {
        "function_item" => "function",
        "struct_item" => "struct",
        "enum_item" => "enum",
        "union_item" => "union",
        "type_item" => "type",
        "trait_item" => "trait",
        "mod_item" => "module",
        _ => "",
    }
}

/// Map a tree-sitter item node kind to our schema kind + id prefix. `None` for non-item nodes.
fn item_kind(ts_kind: &str) -> Option<(Kind, &'static str)> {
    match ts_kind {
        "function_item" => Some((Kind::Function, "fn")),
        "struct_item" | "enum_item" | "union_item" | "type_item" => Some((Kind::Type, "type")),
        "trait_item" => Some((Kind::Trait, "trait")),
        "mod_item" => Some((Kind::Module, "mod")),
        _ => None,
    }
}

/// The base identifier of a named type field on `node` (`type` = the self type, `trait` = the
/// implemented trait). `From<&Hit>` → `From`, `Vec<T>` → `Vec`, `a::B` → `B`.
fn field_type_name(node: TsNode, field: &str, src: &str) -> Option<String> {
    let f = node.child_by_field_name(field)?;
    first_type_identifier(f, src)
}

/// First `type_identifier` in DFS order — the base name of a (possibly generic/scoped) type expr.
fn first_type_identifier(node: TsNode, src: &str) -> Option<String> {
    if node.kind() == "type_identifier" {
        return Some(src[node.byte_range()].to_string());
    }
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        if let Some(found) = first_type_identifier(child, src) {
            return Some(found);
        }
    }
    None
}

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

    fn first_of_kind<'a>(node: TsNode<'a>, kind: &str) -> Option<TsNode<'a>> {
        if node.kind() == kind {
            return Some(node);
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            if let Some(found) = first_of_kind(child, kind) {
                return Some(found);
            }
        }
        None
    }

    fn summary_of(src: &str, kind: &str) -> String {
        let mut parser = Parser::new();
        parser
            .set_language(&tree_sitter_rust::LANGUAGE.into())
            .unwrap();
        let tree = parser.parse(src, None).unwrap();
        let node = first_of_kind(tree.root_node(), kind).expect("node kind present");
        signature_summary(node, src)
    }

    #[test]
    fn fn_summary_has_params_and_return() {
        let s = summary_of(
            "pub fn band_anchored(score: i64, anchor: f64) -> Confidence { todo!() }",
            "function_item",
        );
        assert_eq!(
            s,
            "band_anchored — accepts: score, anchor. Returns: Confidence"
        );
    }

    #[test]
    fn fn_summary_skips_self_receiver() {
        let s = summary_of(
            "impl X { fn run(&self, n: u32) -> bool { true } }",
            "function_item",
        );
        assert_eq!(s, "run — accepts: n. Returns: bool");
    }

    #[test]
    fn struct_and_enum_summaries() {
        assert_eq!(
            summary_of("pub struct RetryPolicy { max: u32 }", "struct_item"),
            "RetryPolicy — struct"
        );
        assert_eq!(summary_of("enum Mode { A, B }", "enum_item"), "Mode — enum");
    }
}
