use std::collections::{HashMap, HashSet};
use std::path::Path;

use eidos_kernel::schema::relation::CONTAINS;
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, qualified_id,
    span_evidence,
};
use super::imports::{collect_python_imports, import_ref};
use super::intent::generic_code_intent_aliases;
use super::python_fastapi::{collect_fastapi_names, python_class_aliases, python_routes};

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_python::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 last_line = src.lines().count().max(1);
    let file_mod_id = format!("mod.{modpath}");
    let fastapi_names = collect_fastapi_names(tree.root_node(), &src);
    let imports = collect_python_imports(tree.root_node(), &src);
    let title = module_title(path);
    let summary = module_summary(leading_py_doc(&src), "python", &title, &modpath);
    let mut module_aliases = vec!["module".to_string(), "python".to_string()];
    module_aliases.extend(generic_code_intent_aliases(&title, &modpath, None, ""));
    module_aliases.sort();
    module_aliases.dedup();
    fg.nodes.push(Node {
        id: file_mod_id.clone(),
        kind: Kind::Module,
        subkind: None,
        title,
        summary,
        aliases: module_aliases,
        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,
        }),
    });

    // Per-class `self.<attr>` → inferred type, so `self.memory.append()` resolves to the specific
    // `Memory.append` even when `append` is ambiguous across classes.
    let attr_types = python_class_attr_types(tree.root_node(), &src);

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

#[allow(clippy::too_many_arguments)]
fn walk_python(
    node: TsNode,
    src: &str,
    path: &str,
    modpath: &str,
    container_id: &str,
    class_qual: Option<&str>,
    fastapi_names: &HashSet<String>,
    imports: &ImportMap,
    attr_types: &HashMap<String, HashMap<String, String>>,
    fg: &mut FileGraph,
) {
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        if emit_python_decl(
            child,
            child,
            src,
            path,
            modpath,
            container_id,
            class_qual,
            fastapi_names,
            imports,
            attr_types,
            fg,
        ) {
            continue;
        }
        walk_python(
            child,
            src,
            path,
            modpath,
            container_id,
            class_qual,
            fastapi_names,
            imports,
            attr_types,
            fg,
        );
    }
}

#[allow(clippy::too_many_arguments)]
fn emit_python_decl(
    node: TsNode,
    span_node: TsNode,
    src: &str,
    path: &str,
    modpath: &str,
    container_id: &str,
    class_qual: Option<&str>,
    fastapi_names: &HashSet<String>,
    imports: &ImportMap,
    attr_types: &HashMap<String, HashMap<String, String>>,
    fg: &mut FileGraph,
) -> bool {
    if node.kind() == "decorated_definition" {
        let Some(def) = node.child_by_field_name("definition") else {
            return false;
        };
        return emit_python_decl(
            def,
            node,
            src,
            path,
            modpath,
            container_id,
            class_qual,
            fastapi_names,
            imports,
            attr_types,
            fg,
        );
    }

    match node.kind() {
        "function_definition" => {
            let Some(name) = python_node_name(node, src) else {
                return false;
            };
            let id = qualified_id("fn", modpath, class_qual, &name);
            emit_python_node(
                id,
                Kind::Function,
                &name,
                span_node,
                src,
                path,
                container_id,
                python_function_aliases(
                    &name,
                    node,
                    span_node,
                    src,
                    modpath,
                    class_qual.is_some(),
                    fastapi_names,
                ),
                imports,
                attr_types,
                fg,
            );
            true
        }
        "class_definition" => {
            let Some(name) = python_node_name(node, src) else {
                return false;
            };
            let id = qualified_id("type", modpath, class_qual, &name);
            emit_python_node(
                id.clone(),
                Kind::Type,
                &name,
                span_node,
                src,
                path,
                container_id,
                python_class_aliases(node, src, modpath),
                imports,
                attr_types,
                fg,
            );
            walk_python(
                node,
                src,
                path,
                modpath,
                &id,
                Some(&name),
                fastapi_names,
                imports,
                attr_types,
                fg,
            );
            true
        }
        _ => false,
    }
}

#[allow(clippy::too_many_arguments)]
fn emit_python_node(
    id: String,
    kind: Kind,
    name: &str,
    span_node: TsNode,
    src: &str,
    path: &str,
    container_id: &str,
    aliases: Vec<String>,
    imports: &ImportMap,
    attr_types: &HashMap<String, HashMap<String, String>>,
    fg: &mut FileGraph,
) {
    fg.nodes.push(Node {
        id: id.clone(),
        kind,
        subkind: None,
        title: name.to_string(),
        summary: {
            let doc = doc_comment_above_py(span_node, src);
            if !doc.is_empty() {
                doc
            } else {
                signature_summary(span_node, src)
            }
        },
        aliases,
        tags: Vec::new(),
        query_examples: Vec::new(),
        source_files: vec![path.to_string()],
        partition: None,
        span: Some(Span {
            path: path.to_string(),
            start_line: span_node.start_position().row + 1,
            end_line: span_node.end_position().row + 1,
        }),
    });
    fg.edges.push(Edge {
        from: container_id.to_string(),
        to: id.clone(),
        relation: CONTAINS.into(),
        evidence: span_evidence(path, span_node),
        // Syntax (class/module containment) — earned Resolved.
        basis: eidos_kernel::schema::EdgeBasis::Resolved,
        ..Default::default()
    });
    if matches!(kind, Kind::Function | Kind::Type | Kind::Trait) {
        // The enclosing class is the id segment before the symbol name (`fn.mod::Class::method`);
        // its `self.<attr>` types disambiguate the method calls in this body.
        let empty = HashMap::new();
        let class_attrs = id
            .rsplit("::")
            .nth(1)
            .and_then(|cls| attr_types.get(cls))
            .unwrap_or(&empty);
        let mut idents = Vec::new();
        collect_python_idents(
            span_node,
            src,
            imports,
            class_attrs,
            &mut idents,
            &mut HashSet::new(),
        );
        fg.refs.push((id, idents));
    }
}

fn python_node_name(node: TsNode, src: &str) -> Option<String> {
    let name = node.child_by_field_name("name")?;
    (name.kind() == "identifier").then(|| src[name.byte_range()].to_string())
}

fn python_function_aliases(
    name: &str,
    function: TsNode,
    span_node: TsNode,
    src: &str,
    modpath: &str,
    method: bool,
    fastapi_names: &HashSet<String>,
) -> Vec<String> {
    let mut aliases = vec!["python".to_string(), "function".to_string()];
    if let Some(words) = name_words(name) {
        aliases.push(format!("{words} function"));
        aliases.push(format!("{words} python function"));
    }
    if method {
        aliases.push("method".to_string());
        if let Some(words) = name_words(name) {
            aliases.push(format!("{words} method"));
        }
    }
    if name_contains_word(name, "fixture") {
        aliases.push("fixture".to_string());
        aliases.push("test fixture".to_string());
    }
    if name_contains_word(name, "config") {
        aliases.push("config".to_string());
        aliases.push("configuration".to_string());
    }
    if is_service_mod(modpath) {
        aliases.push("service".to_string());
        if let Some(words) = name_words(name) {
            aliases.push(format!("{words} service"));
            aliases.push(format!("{words} python service"));
        }
    }
    if python_is_async(function) {
        aliases.push("async function".to_string());
    }
    aliases.extend(generic_code_intent_aliases(
        name,
        modpath,
        Some(span_node),
        src,
    ));
    for route in python_routes(span_node, src, fastapi_names) {
        aliases.push("route".to_string());
        aliases.push("route handler".to_string());
        aliases.push("api route".to_string());
        aliases.push("endpoint".to_string());
        aliases.push("api endpoint".to_string());
        aliases.push("fastapi route".to_string());
        aliases.push("fastapi".to_string());
        if let Some(subject) = route_subject_words(&route.path) {
            aliases.push(format!("{subject} route"));
            aliases.push(format!("api {subject} route"));
            aliases.push(format!("fastapi {subject} route"));
            aliases.push(format!("{subject} endpoint"));
            aliases.push(format!("api {subject} endpoint"));
        }
        aliases.push(route.method.clone());
        aliases.push(route.method_path());
        aliases.push(route.path);
    }
    aliases.sort();
    aliases.dedup();
    aliases
}

fn is_service_mod(modpath: &str) -> bool {
    modpath.split("::").any(|part| {
        matches!(part, "service" | "services") || part_has_token(part, &["service", "services"])
    })
}

fn name_words(name: &str) -> Option<String> {
    let words: Vec<&str> = name
        .split('_')
        .filter(|part| part.chars().count() > 1)
        .collect();
    (words.len() >= 2).then(|| words.join(" "))
}

fn name_contains_word(name: &str, want: &str) -> bool {
    name_words(name).is_some_and(|words| words.split_whitespace().any(|word| word == want))
}

fn part_has_token(part: &str, wanted: &[&str]) -> bool {
    part.split(['_', '-', '.'])
        .any(|token| wanted.contains(&token))
}

fn python_is_async(function: TsNode) -> bool {
    let mut cursor = function.walk();
    function
        .children(&mut cursor)
        .any(|child| child.kind() == "async")
}

fn route_subject_words(path: &str) -> Option<String> {
    let mut words = Vec::new();
    for raw in path.split(|c: char| !c.is_alphanumeric()) {
        if raw.len() <= 1 || raw == "id" || raw.ends_with("_id") {
            continue;
        }
        let word = raw
            .strip_suffix('s')
            .filter(|singular| singular.len() > 2)
            .unwrap_or(raw);
        if !words.iter().any(|w| w == word) {
            words.push(word.to_string());
        }
    }
    (words.len() <= 2 && !words.is_empty()).then(|| words.join(" "))
}

fn collect_python_idents(
    node: TsNode,
    src: &str,
    imports: &ImportMap,
    attr_types: &HashMap<String, String>,
    out: &mut Vec<RefName>,
    seen: &mut HashSet<String>,
) {
    // A `self.<attr>.<method>(...)` call whose receiver type is known → a receiver-typed method
    // reference, so the resolver can pick the right `<Type>::<method>` instead of dropping an
    // ambiguous bare name. Additive to the generic ident walk below.
    if node.kind() == "call"
        && let Some(recv) = self_attr_method_call(node, src)
        && let Some(ty) = attr_types.get(&recv.attr)
    {
        let key = format!("{ty}::{}", recv.method);
        if seen.insert(key) {
            out.push(RefName::on_receiver(recv.method, ty.clone()));
        }
    }
    if node.kind() == "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_python_idents(child, src, imports, attr_types, out, seen);
    }
}

struct SelfAttrCall {
    attr: String,
    method: String,
}

/// Match a `self.<attr>.<method>(...)` call, returning `(attr, method)`. `None` for any other shape.
fn self_attr_method_call(call: TsNode, src: &str) -> Option<SelfAttrCall> {
    let func = call.child_by_field_name("function")?;
    if func.kind() != "attribute" {
        return None;
    }
    let method = func.child_by_field_name("attribute")?;
    let obj = func.child_by_field_name("object")?;
    if obj.kind() != "attribute" {
        return None;
    }
    let inner = obj.child_by_field_name("object")?;
    if inner.kind() != "identifier" || &src[inner.byte_range()] != "self" {
        return None;
    }
    let attr = obj.child_by_field_name("attribute")?;
    Some(SelfAttrCall {
        attr: src[attr.byte_range()].to_string(),
        method: src[method.byte_range()].to_string(),
    })
}

/// Infer, per class, the type of each `self.<attr>` — from `__init__` parameter annotations that
/// flow into `self.<attr> = <param>`, from constructor calls `self.<attr> = <Type>(...)`, and from
/// annotated assignments `self.<attr>: <Type> = ...`. Best-effort: an un-inferable attr is absent
/// (its method calls stay ambiguous, as before), so this only ever ADDS resolutions.
fn python_class_attr_types(root: TsNode, src: &str) -> HashMap<String, HashMap<String, String>> {
    let mut out = HashMap::new();
    let mut stack = vec![root];
    while let Some(node) = stack.pop() {
        if node.kind() == "class_definition"
            && let Some(name) = python_node_name(node, src)
        {
            out.insert(name, class_attr_types(node, src));
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            stack.push(child);
        }
    }
    out
}

fn class_attr_types(class: TsNode, src: &str) -> HashMap<String, String> {
    let mut param_types: HashMap<String, String> = HashMap::new();
    if let Some(init) = find_method(class, "__init__", src)
        && let Some(params) = init.child_by_field_name("parameters")
    {
        let mut c = params.walk();
        for p in params.children(&mut c) {
            if let Some(pname) = p.child_by_field_name("name")
                && let Some(pty) = p.child_by_field_name("type")
                && let Some(ty) = unwrap_py_type(pty, src)
            {
                param_types.insert(src[pname.byte_range()].to_string(), ty);
            }
        }
    }
    let mut attrs: HashMap<String, String> = HashMap::new();
    let mut stack = vec![class];
    while let Some(node) = stack.pop() {
        if node.kind() == "assignment"
            && let Some(lhs) = node.child_by_field_name("left")
            && let Some(attr) = self_attribute_name(lhs, src)
        {
            let ty = node
                .child_by_field_name("type")
                .and_then(|t| unwrap_py_type(t, src))
                .or_else(|| {
                    node.child_by_field_name("right")
                        .and_then(|rhs| rhs_type(rhs, src, &param_types))
                });
            if let Some(ty) = ty {
                attrs.entry(attr).or_insert(ty);
            }
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            stack.push(child);
        }
    }
    attrs
}

/// `self.<name>` → `Some(name)`.
fn self_attribute_name(node: TsNode, src: &str) -> Option<String> {
    if node.kind() != "attribute" {
        return None;
    }
    let obj = node.child_by_field_name("object")?;
    if obj.kind() != "identifier" || &src[obj.byte_range()] != "self" {
        return None;
    }
    let attr = node.child_by_field_name("attribute")?;
    Some(src[attr.byte_range()].to_string())
}

/// Infer a type from an assignment RHS: a param reference with a known annotation, or a constructor
/// call `Type(...)`.
fn rhs_type(rhs: TsNode, src: &str, param_types: &HashMap<String, String>) -> Option<String> {
    let mut stack = vec![rhs];
    let mut ctor: Option<String> = None;
    while let Some(node) = stack.pop() {
        if node.kind() == "identifier"
            && let Some(ty) = param_types.get(&src[node.byte_range()])
        {
            return Some(ty.clone()); // param annotation is the strongest signal
        }
        if node.kind() == "call"
            && let Some(func) = node.child_by_field_name("function")
            && func.kind() == "identifier"
        {
            let n = &src[func.byte_range()];
            if n.chars().next().is_some_and(char::is_uppercase) {
                ctor = Some(n.to_string());
            }
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            stack.push(child);
        }
    }
    ctor
}

/// Unwrap a type annotation to its salient user type: `Optional[Memory]`→`Memory`,
/// `List[Tool]`→`Tool`, `Memory`→`Memory`. Typing containers are skipped; lowercase builtins fall
/// through harmlessly (no node will match them).
fn unwrap_py_type(ty: TsNode, src: &str) -> Option<String> {
    const CONTAINERS: &[&str] = &[
        "Optional",
        "List",
        "Dict",
        "Tuple",
        "Set",
        "Sequence",
        "Iterable",
        "Mapping",
        "Union",
        "FrozenSet",
        "Type",
        "Awaitable",
        "Coroutine",
    ];
    let mut best: Option<String> = None;
    let mut stack = vec![ty];
    while let Some(node) = stack.pop() {
        if node.kind() == "identifier" {
            let tok = &src[node.byte_range()];
            if !CONTAINERS.contains(&tok) {
                best = Some(tok.to_string());
            }
        }
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            stack.push(child);
        }
    }
    best
}

/// Find a named method (`function_definition`) directly inside a class body.
fn find_method<'a>(class: TsNode<'a>, want: &str, src: &str) -> Option<TsNode<'a>> {
    let body = class.child_by_field_name("body")?;
    let mut cursor = body.walk();
    for child in body.children(&mut cursor) {
        let def = if child.kind() == "decorated_definition" {
            child.child_by_field_name("definition")?
        } else {
            child
        };
        if def.kind() == "function_definition"
            && python_node_name(def, src).as_deref() == Some(want)
        {
            return Some(def);
        }
    }
    None
}

/// Derive a one-line semantic summary from a def/class signature — the fallback when there is no
/// leading `#` comment. `def`/`async def` → `name — accepts: …`; `class` → `Name — class`. Gives
/// BM25F an identity surface for undocumented Python. A real comment always wins.
fn signature_summary(node: TsNode, src: &str) -> String {
    // The signature line — skip any decorator lines that lead a decorated definition.
    let sig_line = src[node.byte_range()]
        .lines()
        .map(str::trim)
        .find(|l| l.starts_with("def ") || l.starts_with("async def ") || l.starts_with("class "))
        .unwrap_or("")
        .to_string();

    let callable = sig_line
        .strip_prefix("async def ")
        .or_else(|| sig_line.strip_prefix("def "));
    if let Some(after) = callable {
        let name = after.split('(').next().unwrap_or("").trim();
        let param_list: Vec<&str> = paren_contents(&sig_line)
            .split(',')
            .map(str::trim)
            .filter(|p| !p.is_empty() && *p != "self" && *p != "cls")
            .collect();
        if !param_list.is_empty() {
            return cap_summary(format!("{name} — accepts: {}", param_list.join(", ")));
        }
        if let Some(ret) = sig_line.split("->").nth(1) {
            let ret = ret.trim().trim_end_matches(':').trim();
            if !ret.is_empty() {
                return cap_summary(format!("{name} — returns: {ret}"));
            }
        }
        return cap_summary(name.to_string());
    }

    if let Some(rest) = sig_line.strip_prefix("class ") {
        let name = rest.split(['(', ':']).next().unwrap_or("").trim();
        if !name.is_empty() {
            return cap_summary(format!("{name} — class"));
        }
    }

    cap_summary(sig_line)
}

fn doc_comment_above_py(item: TsNode, src: &str) -> String {
    let mut lines: Vec<String> = Vec::new();
    let mut sib = item.prev_sibling();
    while let Some(s) = sib {
        if s.kind() != "comment" {
            break;
        }
        let text = src[s.byte_range()].trim();
        let Some(rest) = text.strip_prefix('#') else {
            break;
        };
        lines.push(rest.trim().to_string());
        sib = s.prev_sibling();
    }
    lines.reverse();
    lines.join(" ")
}

fn leading_py_doc(src: &str) -> String {
    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()
}
