//! Rust extractor.
//!
//! Walks the tree-sitter syntax tree and emits one `Symbol` per declaration
//! plus best-effort `Calls`/`Imports`/`Implements` edges. MVP — full
//! crate-local resolver is a follow-up; qnames are file-relative.

mod imports;
mod calls;
mod walk;

use std::path::Path;

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

use crate::ParseOutput;

// Re-export `text` so `crate::framework::axum` can reach it at the same
// path as before the split (`crate::lang::rust::text`).
pub(crate) use crate::util::text;

// Re-exports for tests via `use super::*`.
#[cfg(test)]
pub(crate) use ckg_core::{Edge, EdgeKind};

/// Derive the Rust module path prefix from a file path using Cargo conventions:
///   `src/lib.rs` / `src/main.rs` / `src/<crate>/mod.rs` → `crate`
///   `src/foo.rs`                                       → `crate::foo`
///   `src/foo/mod.rs`                                   → `crate::foo`
///   `src/foo/bar.rs`                                   → `crate::foo::bar`
///   no `src/` segment → fall back to file-path components.
fn path_to_qname_prefix(path: &Path) -> String {
    let parts: Vec<String> = path
        .with_extension("")
        .components()
        .filter_map(|c| c.as_os_str().to_str().map(str::to_owned))
        .collect();

    // Find the LAST `src` segment (handles nested workspaces like
    // `crates/foo/src/lib.rs`).
    let after_src = parts
        .iter()
        .rposition(|p| p == "src")
        .map(|i| &parts[i + 1..]);

    let segments: Vec<&str> = match after_src {
        Some(rest) => {
            // Drop trailing `lib` / `main` / `mod` (these are the file-name
            // for the parent module, not a new segment).
            let mut v: Vec<&str> = rest.iter().map(String::as_str).collect();
            if matches!(v.last().copied(), Some("lib") | Some("main") | Some("mod")) {
                v.pop();
            }
            v
        }
        None => parts.iter().map(String::as_str).collect(),
    };
    if segments.is_empty() {
        "crate".to_string()
    } else {
        format!("crate::{}", segments.join("::"))
    }
}

pub(crate) fn child_named<'a>(n: Node<'a>, kind: &str) -> Option<Node<'a>> {
    let mut c = n.walk();
    n.children(&mut c).find(|x| x.kind() == kind)
}

pub(crate) fn name_of(n: Node<'_>, src: &str) -> Option<String> {
    n.child_by_field_name("name")
        .or_else(|| child_named(n, "identifier"))
        .or_else(|| child_named(n, "type_identifier"))
        .map(|x| text(x, src).to_string())
}

pub(crate) fn is_public(n: Node<'_>, src: &str) -> bool {
    let mut c = n.walk();
    n.children(&mut c)
        .any(|x| x.kind() == "visibility_modifier" && text(x, src).starts_with("pub"))
}

pub(crate) fn collect_doc(n: Node<'_>, src: &str) -> String {
    let mut docs = Vec::new();
    let mut is_test = false;
    let mut cursor = n;
    while let Some(prev) = cursor.prev_sibling() {
        let k = prev.kind();
        if k == "line_comment" || k == "block_comment" {
            let body = text(prev, src);
            if body.starts_with("///") || body.starts_with("//!") {
                docs.push(body.trim_start_matches('/').trim().to_string());
                cursor = prev;
                continue;
            }
        }
        // Test markers: `#[test]`, `#[tokio::test]`, `#[cfg(test)]`, etc.
        // Tag the symbol so `dead-code` consumers can exclude tests
        // (otherwise every #[test] looks unreferenced — tree-sitter can't
        // see the test harness as a caller).
        if k == "attribute_item" {
            let body = text(prev, src);
            if body.contains("test") {
                is_test = true;
            }
            cursor = prev;
            continue;
        }
        break;
    }
    docs.reverse();
    let mut out = docs.join("\n");
    if is_test {
        if !out.is_empty() {
            out.push(' ');
        }
        out.push_str("@test");
    }
    out
}

pub(crate) fn map_kind(node_kind: &str) -> Option<Kind> {
    Some(match node_kind {
        "function_item" | "function_signature_item" => Kind::Function,
        "struct_item" => Kind::Struct,
        "enum_item" => Kind::Enum,
        "trait_item" => Kind::Trait,
        "mod_item" => Kind::Module,
        "const_item" | "static_item" => Kind::Constant,
        // `type_item` is the file-scope `type Foo = …` form; `associated_type`
        // is `type Item;` / `type Item = …;` inside a trait or impl block.
        "type_item" | "associated_type" => Kind::TypeAlias,
        "macro_definition" => Kind::Function,
        _ => return None,
    })
}

pub(crate) struct Ctx<'a> {
    pub(crate) repo_id: &'a str,
    pub(crate) relpath: String,
    pub(crate) qname_prefix: String,
    pub(crate) out: ParseOutput,
    pub(crate) name_to_id: std::collections::HashMap<String, String>,
    pub(crate) use_aliases: std::collections::HashMap<String, String>,
    /// Inside `impl Type { … }`, the `Type` name. Lets `self.method()`
    /// in method bodies resolve to `Type.method` at conf 0.7 instead of
    /// the leaf-only fallback at 0.4.
    pub(crate) impl_self_type: Option<String>,
    /// `(src_id, raw_target, is_awaited)` — when `is_awaited=true`, the
    /// resolver emits an `Awaits` edge instead of `Calls`.
    pub(crate) raw_calls: Vec<(String, String, bool)>,
    /// `type Foo = Bar;` — `Foo` → `Bar`. Used by the resolver to follow
    /// type-alias chains when computing `Type::method` dispatch.
    pub(crate) type_aliases: std::collections::HashMap<String, String>,
    /// CR-parse-H-3: O(1) `id → out.symbols` index. Same shape as
    /// ruby::Ctx::id_to_idx — drops post-emit doc-tagging from O(n²)
    /// to O(n) over the whole walk.
    pub(crate) id_to_idx: std::collections::HashMap<String, usize>,
}

impl Ctx<'_> {
    pub(crate) fn push_symbol(
        &mut self,
        node: Node<'_>,
        src: &str,
        kind: Kind,
        qname: String,
        name: String,
    ) -> String {
        let pos = node.start_position();
        let id = node_id(self.repo_id, "rust", &self.relpath, &qname, kind);
        self.name_to_id.insert(name.clone(), id.clone());
        let idx = self.out.symbols.len();
        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,
            is_public: is_public(node, src),
            doc: collect_doc(node, src),
            hash: blake3::hash(text(node, src).as_bytes()).to_hex()[..16].to_string(),
        });
        self.id_to_idx.insert(id.clone(), idx);
        id
    }

    /// CR-parse-H-3: O(1) lookup helper for the post-emit doc-tagging
    /// pattern (rust/walk.rs:300).
    pub(crate) fn symbol_by_id_mut(&mut self, id: &str) -> Option<&mut Symbol> {
        let idx = *self.id_to_idx.get(id)?;
        self.out.symbols.get_mut(idx)
    }
}

// Axum route detection lives in `crate::framework::axum`. The walk hook stays
// in walk.rs (next to the call-detection logic) and dispatches over there.

pub fn extract(repo_id: &str, path: &Path, source: &str) -> Result<ParseOutput> {
    let lang: tree_sitter::Language = tree_sitter_rust::LANGUAGE.into();
    let mut parser = crate::util::get_pooled_parser("rust", &lang);
    let tree = crate::util::guarded_parse(&mut parser, source, "rust");
    // 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 relpath = crate::util::path_to_relpath(path)?;
    let qname_prefix = path_to_qname_prefix(path);
    let mut ctx = Ctx {
        repo_id,
        relpath,
        qname_prefix,
        out: ParseOutput::default(),
        name_to_id: std::collections::HashMap::new(),
        use_aliases: std::collections::HashMap::new(),
        impl_self_type: None,
        raw_calls: Vec::new(),
        type_aliases: std::collections::HashMap::new(),
        id_to_idx: std::collections::HashMap::new(),
    };
    walk::walk(&mut ctx, tree.root_node(), source, "");
    calls::resolve_calls(&mut ctx);
    crate::util::return_pooled_parser("rust", parser);
    Ok(ctx.out)
}

#[cfg(test)]
mod tests;
