//! Rust `use` declaration processing — emits one `Imports` edge per
//! resolved symbol and builds the alias map used by the call resolver
//! for 0.9-confidence dispatch.

use ckg_core::{Edge, EdgeKind};
use tree_sitter::Node;

use super::Ctx;
use crate::util::text;

/// CR-M-3 (closed): emits ONE `Imports` edge per resolved fully-qualified
/// path inside the `use` body. Previously emitted a single edge whose
/// `dst` was the unsplit `use foo::{a, b, c}` body string — coarse for
/// cross-repo import-graph queries because consumers had to re-parse
/// the blob. Now `dst` is the fully-qualified path (e.g.
/// `std::collections::HashMap`); the per-row shape lets graph queries
/// `JOIN` against `Symbol.qname` directly.
pub(crate) fn walk_use(ctx: &mut Ctx<'_>, node: Node<'_>, src: &str) {
    let body = text(node, src);
    // PARSE-H4: handle all Rust visibility modifiers, not just bare `pub`.
    // `pub(crate) use foo::Bar` → trim_start_matches("pub") left `(crate) use ...`.
    // Now: strip `pub(...)` or `pub` first, then `use`.
    let after_vis = body.trim_start();
    let after_vis = if let Some(rest) = after_vis.strip_prefix("pub") {
        let rest = rest.trim_start();
        // pub(crate), pub(super), pub(self), pub(in path::to)
        if rest.starts_with('(') {
            if let Some(close) = rest.find(')') {
                rest[close + 1..].trim_start()
            } else {
                rest // malformed, best-effort
            }
        } else {
            rest
        }
    } else {
        after_vis
    };
    let trimmed = after_vis
        .trim_start_matches("use")
        .trim()
        .trim_end_matches(';')
        .trim();
    // CR-review-#2: cheap defensive fix for nested brace groups (e.g.
    // `use std::sync::{atomic::{AtomicUsize, Ordering}, Mutex};`).
    // The flat splitter at `walk_use_paths` would produce garbage `dst`
    // rows on nested groups. Until tree-sitter children walking lands,
    // refuse to parse nested groups and emit only the coarse fallback.
    // Detection: brace-after-brace inside the body.
    let nested = has_nested_braces(trimmed);
    let mut entries: Vec<(String, String)> = Vec::new();
    if !nested {
        walk_use_paths(trimmed, &mut entries);
    }
    // CR-review-#1: also detect glob presence in the raw body so a
    // mixed-group like `use foo::{*, helper}` keeps the `dst.contains('*')`
    // signal for glob-impact queries while still emitting per-symbol
    // edges for the named members.
    let has_glob = trimmed.contains("::*")
        || trimmed.contains("{*")
        || trimmed.contains(", *")
        || trimmed.ends_with('*');
    if entries.is_empty() {
        // `use foo::*;`, `use self;`, or nested-brace fallback. Single
        // coarse edge preserves observability of the import statement.
        ctx.out.edges.push(Edge {
            kind: EdgeKind::Imports,
            src: ctx.relpath.clone(),
            dst: trimmed.to_string(),
            confidence: 0.9,
        });
    } else {
        for (alias_name, full_path) in &entries {
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Imports,
                src: ctx.relpath.clone(),
                dst: full_path.clone(),
                confidence: 0.9,
            });
            ctx.use_aliases
                .insert(alias_name.clone(), full_path.clone());
        }
        if has_glob {
            // Mixed-group glob: emit one extra coarse edge so glob-impact
            // queries still see the `*` signal.
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Imports,
                src: ctx.relpath.clone(),
                dst: trimmed.to_string(),
                confidence: 0.9,
            });
        }
    }
}

/// True if `body` contains a brace group nested inside another brace
/// group — e.g. `use foo::{a::{b, c}, d}`. The flat splitter mishandles
/// these; we fall back to a coarse edge until tree-sitter children
/// walking is wired in (CR-review-#2).
fn has_nested_braces(body: &str) -> bool {
    let Some(first_open) = body.find('{') else {
        return false;
    };
    let after_first = &body[first_open + 1..];
    // Find the matching close brace. A subsequent `{` before that close
    // means nesting.
    let mut depth = 1usize;
    for c in after_first.chars() {
        match c {
            '{' => return true,
            '}' => {
                depth -= 1;
                if depth == 0 {
                    return false;
                }
            }
            _ => {}
        }
    }
    false
}

/// Parse a trimmed `use` body and append `(alias_name, full_path)`
/// pairs to `out`. Shapes handled:
///
/// - `foo::bar::Baz`             → `("Baz", "foo::bar::Baz")`
/// - `foo::bar::Baz as Qux`      → `("Qux", "foo::bar::Baz")`
/// - `foo::bar::{Baz, Qux as Q}` → `("Baz", "foo::bar::Baz")`, `("Q", "foo::bar::Qux")`
/// - `foo::*`, `foo::self`       → no entries (caller falls back to coarse edge)
fn walk_use_paths(body: &str, out: &mut Vec<(String, String)>) {
    if let Some(brace_open) = body.find('{') {
        let prefix = body[..brace_open].trim_end_matches("::").trim();
        let close = body.rfind('}').unwrap_or(body.len());
        let inner = &body[brace_open + 1..close];
        for part in inner.split(',') {
            push_single(out, part.trim(), prefix);
        }
    } else {
        push_single(out, body, "");
    }
}

fn push_single(out: &mut Vec<(String, String)>, item: &str, prefix: &str) {
    let item = item.trim();
    if item.is_empty() || item == "self" || item == "*" {
        return;
    }
    // `<path> as <alias>`
    let (path, alias) = if let Some(idx) = item.find(" as ") {
        (item[..idx].trim(), Some(item[idx + 4..].trim()))
    } else {
        (item, None)
    };
    // Glob imports (`foo::*`, `*`) and `self` re-exports don't bring a
    // single named symbol into scope — skip them so per-symbol Imports
    // edges stay sound. The caller's fallback emits a coarse edge for
    // the entire `use` body so glob-import observability is preserved.
    if path == "*" || path.ends_with("::*") || path == "self" || path.ends_with("::self") {
        return;
    }
    // CR-storage-IMP-2: defensive against malformed input that would
    // otherwise produce empty/junk entries:
    //   `use ::foo;`   → path=`::foo`     (leading `::`)
    //   `use foo::;`   → path=`foo::`     (trailing `::`)
    //   `use foo as ;` → alias=Some("")   (caught here)
    //   `use foo as`   → trailing " as"   (no space after, falls into
    //                                      no-alias branch as path=
    //                                      "foo as"; reject below)
    //   `use ::;`      → path=`::`
    // All would yield empty `name` keys polluting `ctx.use_aliases`
    // and meaningless `dst` strings on Imports edges.
    if path.is_empty() || path.starts_with("::") || path.ends_with("::") {
        return;
    }
    if path.ends_with(" as") || path == "as" {
        return;
    }
    if matches!(alias, Some(a) if a.is_empty()) {
        return;
    }
    let full = if prefix.is_empty() {
        path.to_string()
    } else {
        format!("{prefix}::{path}")
    };
    let name = alias
        .map(str::to_string)
        .unwrap_or_else(|| full.rsplit("::").next().unwrap_or(&full).to_string());
    if name.is_empty() {
        return;
    }
    out.push((name, full));
}

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

    fn run(body: &str) -> Vec<(String, String)> {
        let mut out = Vec::new();
        walk_use_paths(body, &mut out);
        out
    }

    #[test]
    fn single_path_emits_one_entry() {
        let r = run("std::collections::HashMap");
        assert_eq!(r, vec![("HashMap".into(), "std::collections::HashMap".into())]);
    }

    #[test]
    fn aliased_path_uses_alias_as_name() {
        let r = run("std::sync::Arc as A");
        assert_eq!(r, vec![("A".into(), "std::sync::Arc".into())]);
    }

    #[test]
    fn brace_group_emits_one_per_member() {
        let r = run("std::sync::{Arc, Mutex, RwLock as RW}");
        assert_eq!(
            r,
            vec![
                ("Arc".into(), "std::sync::Arc".into()),
                ("Mutex".into(), "std::sync::Mutex".into()),
                ("RW".into(), "std::sync::RwLock".into()),
            ]
        );
    }

    #[test]
    fn glob_and_self_produce_no_entries() {
        assert_eq!(run("foo::*"), Vec::<(String, String)>::new());
        assert_eq!(run("foo::{self, bar}"), vec![("bar".into(), "foo::bar".into())]);
    }

    /// CR-storage-IMP-2 regression: malformed `use` shapes produce no
    /// per-symbol entries (caller fallback emits the coarse edge).
    #[test]
    fn malformed_paths_produce_no_entries() {
        let empty = Vec::<(String, String)>::new();
        assert_eq!(run("::foo"), empty, "leading :: must be skipped");
        assert_eq!(run("foo::"), empty, "trailing :: must be skipped");
        assert_eq!(run("::"), empty, "bare :: must be skipped");
        assert_eq!(run(""), empty, "empty body must be skipped");
        assert_eq!(run("foo as "), empty, "empty alias must be skipped");
        assert_eq!(run("foo::{::bar, baz}"), vec![("baz".into(), "foo::baz".into())]);
    }

    /// CR-review-#2 regression: nested brace groups are detected and
    /// fall back to coarse edge (no garbage rows emitted).
    #[test]
    fn nested_braces_detected() {
        assert!(has_nested_braces("std::sync::{atomic::{AtomicUsize, Ordering}, Mutex}"));
        assert!(has_nested_braces("foo::{a, b::{c, d}}"));
        assert!(!has_nested_braces("std::sync::{Arc, Mutex}"));
        assert!(!has_nested_braces("std::collections::HashMap"));
        assert!(!has_nested_braces("foo::*"));
    }
}
