use std::collections::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 crate::glyph::language::SourceLanguage;

use super::common::{
    cap_summary, mod_path, module_summary, module_title, node_name, paren_contents, qualified_id,
    span_evidence,
};
use super::imports::{collect_ts_imports, import_ref};
use super::intent::generic_code_intent_aliases;
use super::typescript_react::{
    is_component_name, ts_function_aliases_in_mod, ts_react_wrapper_name, ts_type_aliases_in_mod,
};

pub(crate) fn symbols_in_file(path: &Path, base: &Path, language: SourceLanguage) -> FileGraph {
    let mut fg = FileGraph::default();
    let Ok(src) = std::fs::read_to_string(path) else {
        return fg;
    };
    let mut parser = Parser::new();
    let ts_language = match language {
        SourceLanguage::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
        SourceLanguage::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
        SourceLanguage::Rust | SourceLanguage::Python => {
            unreachable!("non-TypeScript languages are dispatched before ts_symbols_in_file")
        }
    };
    if parser.set_language(&ts_language).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 imports = collect_ts_imports(tree.root_node(), &src, &modpath);
    let last_line = src.lines().count().max(1);
    let file_mod_id = format!("mod.{modpath}");
    let title = module_title(path);
    let summary = module_summary(leading_js_doc(&src), language.label(), &title, &modpath);
    let mut module_aliases = vec!["module".to_string(), language.label().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,
        }),
    });

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

#[allow(clippy::too_many_arguments)]
fn walk_ts(
    node: TsNode,
    src: &str,
    path: &str,
    modpath: &str,
    container_id: &str,
    type_qual: Option<&str>,
    language: SourceLanguage,
    imports: &ImportMap,
    fg: &mut FileGraph,
) {
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        if child.kind() == "export_statement" {
            if let Some(decl) = child.child_by_field_name("declaration") {
                emit_ts_decl(
                    decl,
                    child,
                    src,
                    path,
                    modpath,
                    container_id,
                    type_qual,
                    language,
                    imports,
                    true,
                    fg,
                );
            }
            continue;
        }

        if emit_ts_decl(
            child,
            child,
            src,
            path,
            modpath,
            container_id,
            type_qual,
            language,
            imports,
            false,
            fg,
        ) {
            continue;
        }

        walk_ts(
            child,
            src,
            path,
            modpath,
            container_id,
            type_qual,
            language,
            imports,
            fg,
        );
    }
}

#[allow(clippy::too_many_arguments)]
fn emit_ts_decl(
    node: TsNode,
    span_node: TsNode,
    src: &str,
    path: &str,
    modpath: &str,
    container_id: &str,
    type_qual: Option<&str>,
    language: SourceLanguage,
    imports: &ImportMap,
    exported: bool,
    fg: &mut FileGraph,
) -> bool {
    match node.kind() {
        "function_declaration" => {
            let Some(name) = node_name(node, src) else {
                return false;
            };
            let id = qualified_id("fn", modpath, type_qual, &name);
            emit_ts_node(
                id.clone(),
                Kind::Function,
                &name,
                span_node,
                src,
                path,
                container_id,
                ts_function_aliases_in_mod(&name, node, src, exported, language, modpath),
                imports,
                fg,
            );
            true
        }
        "class_declaration" => {
            let Some(name) = node_name(node, src) else {
                return false;
            };
            let id = qualified_id("type", modpath, type_qual, &name);
            emit_ts_node(
                id.clone(),
                Kind::Type,
                &name,
                span_node,
                src,
                path,
                container_id,
                ts_type_aliases_in_mod(&name, "class", exported, language, modpath),
                imports,
                fg,
            );
            walk_ts(
                node,
                src,
                path,
                modpath,
                &id,
                Some(&name),
                language,
                imports,
                fg,
            );
            true
        }
        "interface_declaration" => {
            let Some(name) = node_name(node, src) else {
                return false;
            };
            emit_ts_node(
                qualified_id("type", modpath, type_qual, &name),
                Kind::Type,
                &name,
                span_node,
                src,
                path,
                container_id,
                ts_type_aliases_in_mod(&name, "interface", exported, language, modpath),
                imports,
                fg,
            );
            true
        }
        "type_alias_declaration" => {
            let Some(name) = node_name(node, src) else {
                return false;
            };
            emit_ts_node(
                qualified_id("type", modpath, type_qual, &name),
                Kind::Type,
                &name,
                span_node,
                src,
                path,
                container_id,
                ts_type_aliases_in_mod(&name, "type", exported, language, modpath),
                imports,
                fg,
            );
            true
        }
        "method_definition" => {
            let Some(name) = node_name(node, src) else {
                return false;
            };
            emit_ts_node(
                qualified_id("fn", modpath, type_qual, &name),
                Kind::Function,
                &name,
                span_node,
                src,
                path,
                container_id,
                ts_function_aliases_in_mod(&name, node, src, exported, language, modpath),
                imports,
                fg,
            );
            true
        }
        "lexical_declaration" => {
            let mut emitted = false;
            let mut cursor = node.walk();
            for child in node.children(&mut cursor) {
                if child.kind() != "variable_declarator" {
                    continue;
                }
                let Some(value) = child.child_by_field_name("value") else {
                    continue;
                };
                let Some(name_node) = child.child_by_field_name("name") else {
                    continue;
                };
                if name_node.kind() != "identifier" {
                    continue;
                }
                let name = src[name_node.byte_range()].to_string();
                let wrapper = ts_react_wrapper_name(value, src);
                if !matches!(value.kind(), "arrow_function" | "function_expression")
                    && wrapper.is_none()
                {
                    continue;
                }
                let mut aliases =
                    ts_function_aliases_in_mod(&name, child, src, exported, language, modpath);
                if let Some(wrapper) = wrapper {
                    aliases.push("react wrapper".to_string());
                    aliases.push(wrapper.clone());
                    aliases.push(format!("react {wrapper}"));
                    if is_component_name(&name) {
                        aliases.push("component".to_string());
                        aliases.push("react component".to_string());
                    }
                    aliases.sort();
                    aliases.dedup();
                }
                emit_ts_node(
                    qualified_id("fn", modpath, type_qual, &name),
                    Kind::Function,
                    &name,
                    span_node,
                    src,
                    path,
                    container_id,
                    aliases,
                    imports,
                    fg,
                );
                emitted = true;
            }
            emitted
        }
        _ => false,
    }
}

#[allow(clippy::too_many_arguments)]
fn emit_ts_node(
    id: String,
    kind: Kind,
    name: &str,
    span_node: TsNode,
    src: &str,
    path: &str,
    container_id: &str,
    aliases: Vec<String>,
    imports: &ImportMap,
    fg: &mut FileGraph,
) {
    fg.nodes.push(Node {
        id: id.clone(),
        kind,
        subkind: None,
        title: name.to_string(),
        summary: {
            let doc = doc_comment_above_ts(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 (module/namespace containment) — earned Resolved.
        basis: eidos_kernel::schema::EdgeBasis::Resolved,
        ..Default::default()
    });
    if matches!(kind, Kind::Function | Kind::Type | Kind::Trait) {
        let mut idents = Vec::new();
        collect_ts_idents(span_node, src, imports, &mut idents, &mut HashSet::new());
        fg.refs.push((id, idents));
    }
}

fn collect_ts_idents(
    node: TsNode,
    src: &str,
    imports: &ImportMap,
    out: &mut Vec<RefName>,
    seen: &mut HashSet<String>,
) {
    if matches!(
        node.kind(),
        "identifier" | "type_identifier" | "property_identifier" | "shorthand_property_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_ts_idents(child, src, imports, out, seen);
    }
}

/// Derive a one-line semantic summary from a declaration's signature — the fallback for code with
/// no leading doc comment. Functions/methods → `name — accepts: …. Returns: …`; interfaces/type
/// literals → `Name — fields: …`; classes → `Name — class`. A real doc comment always wins; this
/// only gives BM25F an identity surface where the author left the summary empty.
fn signature_summary(node: TsNode, src: &str) -> String {
    let first_line = first_decl_line(&src[node.byte_range()]);
    let cleaned = strip_ts_prefixes(&first_line);

    // Interfaces and object type aliases: surface the field names.
    if cleaned.starts_with("interface ") || cleaned.starts_with("type ") {
        let fields = ts_field_names(node, src);
        let type_name = cleaned
            .trim_start_matches("interface ")
            .trim_start_matches("type ")
            .split(['{', '<', '=', '('])
            .next()
            .unwrap_or("")
            .trim();
        if !fields.is_empty() {
            return cap_summary(format!("{type_name} — fields: {}", fields.join(", ")));
        }
        return cap_summary(cleaned);
    }

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

    // Functions and methods: name + params + return type.
    if let Some(p) = cleaned.find('(') {
        let name = cleaned[..p].split_whitespace().last().unwrap_or("").trim();
        let param_names: Vec<&str> = paren_contents(&cleaned)
            .split(',')
            .map(|raw| {
                raw.split(':')
                    .next()
                    .unwrap_or("")
                    .trim()
                    .trim_start_matches("public ")
                    .trim_start_matches("private ")
                    .trim_start_matches("protected ")
                    .trim_start_matches("readonly ")
                    .trim()
                    .trim_start_matches("...")
            })
            .filter(|p| !p.is_empty())
            .collect();
        let ret = ts_return_type(&cleaned);
        if !name.is_empty() && !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(", ")),
            });
        }
        if !name.is_empty() {
            return cap_summary(match ret {
                Some(r) => format!("{name} — returns: {r}"),
                None => name.to_string(),
            });
        }
    }

    cap_summary(cleaned)
}

/// The first non-blank, non-decorator line of a declaration's text — the signature line.
fn first_decl_line(text: &str) -> String {
    for line in text.lines() {
        let t = line.trim();
        if t.is_empty() || t.starts_with('@') {
            continue;
        }
        return t.to_string();
    }
    String::new()
}

/// Strip the leading modifier keywords (`export`, `async`, `public`, `get`/`set`, …) that precede
/// the meaningful part of a declaration's signature line.
fn strip_ts_prefixes(line: &str) -> String {
    const KW: &[&str] = &[
        "export ",
        "default ",
        "declare ",
        "abstract ",
        "async ",
        "public ",
        "private ",
        "protected ",
        "static ",
        "readonly ",
        "get ",
        "set ",
    ];
    let mut s = line.trim();
    while let Some(rest) = KW.iter().find_map(|kw| s.strip_prefix(kw)) {
        s = rest.trim_start();
    }
    s.to_string()
}

/// The return-type annotation after a signature's parameter list: the text after the final `)` and
/// its `:`, trimmed of the body brace / arrow. `None` when there is no annotation.
fn ts_return_type(line: &str) -> Option<String> {
    let close = line.rfind(')')?;
    let after = &line[close + 1..];
    let colon = after.find(':')?;
    let mut ret = &after[colon + 1..];
    // Cut the annotation off at the body — a block brace or an arrow expression body.
    if let Some(b) = ret.find('{') {
        ret = &ret[..b];
    }
    if let Some(a) = ret.find("=>") {
        ret = &ret[..a];
    }
    let ret = ret.trim();
    (!ret.is_empty()).then(|| ret.to_string())
}

/// DFS-collect the field names of an interface / object type literal (`property_signature` nodes).
fn ts_field_names(node: TsNode, src: &str) -> Vec<String> {
    let mut out = Vec::new();
    collect_field_names(node, src, &mut out);
    out
}

fn collect_field_names(node: TsNode, src: &str, out: &mut Vec<String>) {
    if node.kind() == "property_signature"
        && let Some(name) = node.child_by_field_name("name")
    {
        out.push(src[name.byte_range()].to_string());
    }
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        collect_field_names(child, src, out);
    }
}

fn doc_comment_above_ts(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();
        if let Some(rest) = text.strip_prefix("//") {
            lines.push(rest.trim().to_string());
        } else if text.starts_with("/**") || text.starts_with("/*") {
            lines.push(clean_block_comment(text));
        } else {
            break;
        }
        sib = s.prev_sibling();
    }
    lines.reverse();
    lines.join(" ")
}

fn leading_js_doc(src: &str) -> String {
    let trimmed = src.trim_start();
    if let Some(rest) = trimmed.strip_prefix("//") {
        return rest.lines().next().unwrap_or_default().trim().to_string();
    }
    if (trimmed.starts_with("/**") || trimmed.starts_with("/*"))
        && let Some(end) = trimmed.find("*/")
    {
        return clean_block_comment(&trimmed[..end + 2]);
    }
    String::new()
}

fn clean_block_comment(text: &str) -> String {
    text.trim()
        .trim_start_matches("/**")
        .trim_start_matches("/*")
        .trim_end_matches("*/")
        .lines()
        .map(|line| line.trim().trim_start_matches('*').trim())
        .filter(|line| !line.is_empty())
        .collect::<Vec<_>>()
        .join(" ")
}
