//! Go extractor.
//!
//! Symbols: Function, Method, Struct, Interface, TypeAlias, Constant,
//! Variable. Edges: Imports, Calls (with type-aware dispatch via
//! `var x T` / `x := T{}` / `x := &T{}`), Extends (struct embedding),
//! Defines (struct/interface → fields/methods).
//!
//! Test detection: top-level functions named `TestFoo` /
//! `BenchmarkFoo` / `ExampleFoo` / `FuzzFoo` get the `@test` doc tag,
//! the same opt-out the dead-code query honors for other languages.

mod calls;
mod decls;
mod imports;

use std::path::Path;

use ckg_core::{Kind, Result, Symbol, node_id};
use tree_sitter::Node;

use crate::ParseOutput;
use crate::util::text;

use decls::{handle_function, handle_method, handle_type_decl, handle_const_or_var};
use imports::handle_imports;

pub(crate) struct Ctx<'a> {
    pub(crate) repo_id: &'a str,
    pub(crate) relpath: String,
    pub(crate) package: String,
    pub(crate) out: ParseOutput,
    pub(crate) name_to_id: std::collections::HashMap<String, String>,
    /// Import alias → fully-qualified path. `import "encoding/json"` →
    /// `json` → `encoding/json`. `import j "encoding/json"` → `j` →
    /// `encoding/json`.
    pub(crate) aliases: std::collections::HashMap<String, String>,
    /// Variable name → type name. Populated by `var x Foo`,
    /// `x := Foo{}`, `x := &Foo{}`, `x := new(Foo)`. Pointer indirection
    /// (`*Foo`) is stripped — we want `Foo.method` regardless.
    pub(crate) types: std::collections::HashMap<String, String>,
}

impl Ctx<'_> {
    pub(crate) fn push(
        &mut self,
        n: Node<'_>,
        src: &str,
        kind: Kind,
        qname: String,
        name: String,
        doc: String,
    ) -> String {
        let pos = n.start_position();
        let id = node_id(self.repo_id, "go", &self.relpath, &qname, kind);
        self.name_to_id.insert(name.clone(), id.clone());
        self.out.symbols.push(Symbol {
            id: id.clone(),
            qname,
            name,
            kind,
            file: self.relpath.clone(),
            line: pos.row as u32 + 1,
            col: pos.column as u32,
            // Go's exported-vs-unexported convention: leading capital = exported.
            is_public: true,
            doc,
            // CR-parse-H-1: compute body hash like Rust/Python/Ruby do.
            // Pre-fix this was String::new() so Go file-staleness queries
            // against `Symbol.hash` always fired.
            hash: blake3::hash(crate::util::text(n, src).as_bytes()).to_hex()[..16].to_string(),
        });
        id
    }
}

/// Strip pointer indirection from a Go type name. `*Foo` → `Foo`,
/// `Foo` → `Foo`. Generic type params not supported (rare in v0.1).
pub(crate) fn unwrap_pointer(node: Node<'_>, src: &str) -> Option<String> {
    match node.kind() {
        "type_identifier" | "qualified_type" => Some(text(node, src).to_string()),
        "pointer_type" => {
            let mut c = node.walk();
            node.children(&mut c)
                .find(|x| matches!(x.kind(), "type_identifier" | "qualified_type"))
                .map(|t| text(t, src).to_string())
        }
        _ => None,
    }
}

/// Collect leading `// …` line comments immediately before `n`.
pub(crate) fn collect_doc(n: Node<'_>, src: &str) -> String {
    let mut docs = Vec::new();
    let mut cursor = n;
    while let Some(prev) = cursor.prev_sibling() {
        if prev.kind() == "comment" {
            let body = text(prev, src);
            if let Some(rest) = body.strip_prefix("//") {
                docs.push(rest.trim().to_string());
                cursor = prev;
                continue;
            }
        }
        break;
    }
    docs.reverse();
    docs.join("\n")
}

pub(crate) fn is_test_name(name: &str) -> bool {
    matches!(
        name.chars().next(),
        Some('T') | Some('B') | Some('E') | Some('F')
    ) && (name.starts_with("Test")
        || name.starts_with("Benchmark")
        || name.starts_with("Example")
        || name.starts_with("Fuzz"))
}

pub fn extract(repo_id: &str, path: &Path, source: &str) -> Result<ParseOutput> {
    let lang: tree_sitter::Language = tree_sitter_go::LANGUAGE.into();
    let mut parser = crate::util::get_pooled_parser("go", &lang);
    let tree = crate::util::guarded_parse(&mut parser, source, "go");
    // H11: complete the full tree walk BEFORE returning the parser to the pool.
    // tree-sitter's Tree is independent of Parser today, but tying their
    // lifetimes would silently corrupt walks. Reorder eliminates the invariant
    // dependence entirely. Pool contention cost is negligible (parse >> walk).
    let tree = tree?;

    let mut ctx = Ctx {
        repo_id,
        relpath: crate::util::path_to_relpath(path)?,
        package: String::new(),
        out: ParseOutput::default(),
        name_to_id: std::collections::HashMap::new(),
        aliases: std::collections::HashMap::new(),
        types: std::collections::HashMap::new(),
    };
    walk(&mut ctx, tree.root_node(), source);
    crate::util::return_pooled_parser("go", parser);
    Ok(ctx.out)
}

fn walk(ctx: &mut Ctx<'_>, root: Node<'_>, src: &str) {
    let mut cursor = root.walk();
    for child in root.children(&mut cursor) {
        match child.kind() {
            "package_clause" => {
                let mut c = child.walk();
                if let Some(id) = child
                    .children(&mut c)
                    .find(|x| x.kind() == "package_identifier")
                {
                    ctx.package = text(id, src).to_string();
                }
            }
            "import_declaration" => handle_imports(ctx, child, src),
            "function_declaration" => handle_function(ctx, child, src),
            "method_declaration" => handle_method(ctx, child, src),
            "type_declaration" => handle_type_decl(ctx, child, src),
            "const_declaration" => handle_const_or_var(ctx, child, src, Kind::Constant),
            "var_declaration" => handle_const_or_var(ctx, child, src, Kind::Variable),
            _ => {}
        }
    }
}

#[cfg(test)]
mod tests;
