//! Python declaration extraction helpers:
//! - `emit_class_fields` — dataclass / Pydantic / Enum field symbols
//! - `emit_package_reexports` — `__init__.py` re-export symbols
//! - `extract_module_docstring` — module-level docstring → Module symbol
//! - `collect_dunder_all` / `apply_dunder_all` — `__all__` visibility filter
//! - `emit_lambda_symbols` — anonymous lambda → Function symbol per line

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

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

/// Emit `Constant` Symbols for each top-level class-body field.
///
/// Catches:
/// - dataclass / NamedTuple / Pydantic model **annotated** fields
///   (`name: str = …` / `name: int`)
/// - Enum / IntEnum / StrEnum **bare** members (`RED = 1`)
pub(crate) fn emit_class_fields(
    ctx: &mut Ctx<'_>,
    body: Node<'_>,
    src: &str,
    class_id: &str,
    class_name: &str,
    outer_scope: &str,
) {
    let mut c = body.walk();
    for stmt in body.children(&mut c) {
        if stmt.kind() != "expression_statement" {
            continue;
        }
        let mut sc = stmt.walk();
        for inner in stmt.children(&mut sc) {
            if inner.kind() != "assignment" {
                continue;
            }
            let Some(left) = inner.child_by_field_name("left") else {
                continue;
            };
            if left.kind() != "identifier" {
                continue;
            }
            let field_name = text(left, src).to_string();
            // Skip dunder bookkeeping like `__slots__`, `__init__`.
            if field_name.starts_with("__") && field_name.ends_with("__") {
                continue;
            }
            let qname = if outer_scope.is_empty() {
                format!("{}.{class_name}.{field_name}", ctx.module)
            } else {
                format!("{}.{outer_scope}.{class_name}.{field_name}", ctx.module)
            };
            let id = ctx.push(inner, src, Kind::Constant, qname, field_name, String::new());
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Defines,
                src: class_id.to_string(),
                dst: id,
                confidence: 1.0,
            });
        }
    }
}

/// `__init__.py` makes its package's submodule contents available under the
/// package's qname. For each `from .submod import Name` (or `as Alias`),
/// emit a `Constant`-kind Symbol at `<package>.Alias` pointing to the
/// re-exported submodule member.
pub(crate) fn emit_package_reexports(ctx: &mut Ctx<'_>) {
    // ctx.module is e.g. `pkg.__init__`. Strip the trailing `.__init__`.
    let pkg = ctx
        .module
        .strip_suffix(".__init__")
        .unwrap_or(&ctx.module)
        .to_string();
    let aliases: Vec<(String, String)> = ctx
        .aliases
        .iter()
        .map(|(k, v)| (k.clone(), v.clone()))
        .collect();
    for (alias, full) in aliases {
        // We only re-export names that resolve under our own package. The
        // alias map already absolutized relative imports for us.
        if !full.starts_with(&pkg) {
            continue;
        }
        let qname = format!("{pkg}.{alias}");
        ctx.out.symbols.push(Symbol {
            id: node_id(ctx.repo_id, "python", &ctx.relpath, &qname, Kind::Constant),
            qname,
            name: alias,
            kind: Kind::Constant,
            file: ctx.relpath.clone(),
            line: 1,
            col: 0,
            is_public: true,
            doc: format!("@reexport {full}"),
            hash: blake3::hash(full.as_bytes()).to_hex()[..16].to_string(),
        });
    }
}

/// Collect names listed in top-level `__all__ = [...]` (or `(...)`). Returns
/// `None` if `__all__` isn't defined → no visibility filter applied.
///
/// **L10 limitation**: `__all__ += [...]` extension patterns are NOT tracked.
/// Only the initial `__all__ = [...]` assignment is recognised. Dynamic
/// extensions via `__all__.extend(...)` or `__all__ += [...]` are silently
/// ignored — the caller sees only names from the first assignment.
pub(crate) fn collect_dunder_all(root: Node<'_>, src: &str) -> Option<Vec<String>> {
    let mut c = root.walk();
    for stmt in root.children(&mut c) {
        if stmt.kind() != "expression_statement" {
            continue;
        }
        let mut sc = stmt.walk();
        for inner in stmt.children(&mut sc) {
            if inner.kind() != "assignment" {
                continue;
            }
            let Some(left) = inner.child_by_field_name("left") else {
                continue;
            };
            if text(left, src) != "__all__" {
                continue;
            }
            let Some(right) = inner.child_by_field_name("right") else {
                continue;
            };
            let raw = text(right, src);
            let names: Vec<String> = raw
                .trim_matches(|c: char| matches!(c, '[' | ']' | '(' | ')' | ' '))
                .split(',')
                .map(|s| {
                    s.trim()
                        .trim_matches(|c: char| matches!(c, '\'' | '"'))
                        .to_string()
                })
                .filter(|s| !s.is_empty())
                .collect();
            return Some(names);
        }
    }
    None
}

/// If `__all__` was defined, mark Symbols whose name appears in it as public,
/// others as private. Symbols at scopes beyond top-level are unaffected.
pub(crate) fn apply_dunder_all(symbols: &mut [Symbol], list: &Option<Vec<String>>, module: &str) {
    let Some(names) = list else { return };
    // PARSE-H8: compute the module's own dot count so sub-packages work.
    // e.g. module = "pkg.sub.module" has module_dots = 2, so a top-level
    // symbol "pkg.sub.module.Foo" has qname dots = 3 = module_dots + 1.
    let module_dot_count = module.matches('.').count();
    for s in symbols.iter_mut() {
        let qname_dots = s.qname.matches('.').count();
        // Only adjust top-level Symbols (qname has exactly one more dot than module).
        if qname_dots == module_dot_count + 1 {
            s.is_public = names.iter().any(|n| n == &s.name);
        }
    }
}

/// Capture the module-level docstring (first string literal in the file) and
/// emit a `Module` symbol carrying it. Lets `ckg search` find files by topic.
pub(crate) fn extract_module_docstring(ctx: &mut Ctx<'_>, root: Node<'_>, src: &str) {
    let mut c = root.walk();
    for child in root.children(&mut c) {
        if child.kind() == "expression_statement" {
            let mut sc = child.walk();
            for s in child.children(&mut sc) {
                if s.kind() == "string" {
                    let raw = text(s, src);
                    let doc = raw.trim_matches('"').trim_matches('\'').trim().to_string();
                    let pos = root.start_position();
                    let id = node_id(
                        ctx.repo_id,
                        "python",
                        &ctx.relpath,
                        &ctx.module,
                        Kind::Module,
                    );
                    ctx.name_to_id.insert(ctx.module.clone(), id.clone());
                    ctx.out.symbols.push(Symbol {
                        id,
                        qname: ctx.module.clone(),
                        name: ctx.module.clone(),
                        kind: Kind::Module,
                        file: ctx.relpath.clone(),
                        line: pos.row as u32 + 1,
                        col: pos.column as u32,
                        is_public: true,
                        doc,
                        hash: blake3::hash(text(s, src).as_bytes()).to_hex()[..16].to_string(),
                    });
                    return;
                }
            }
        }
        // First non-extra non-string statement → no module docstring.
        if !child.is_extra() {
            return;
        }
    }
}

/// Emit a `Function` Symbol for every `lambda x: …` expression. Anonymous
/// names are disambiguated by line number: `<lambda@<line>>`.
pub(crate) fn emit_lambda_symbols(ctx: &mut Ctx<'_>, root: Node<'_>, src: &str) {
    let mut cursor = root.walk();
    let mut stack = vec![root];
    while let Some(n) = stack.pop() {
        // Both the lambda expression AND the `lambda` keyword token share
        // the kind name; only the expression has named children.
        if n.kind() == "lambda" && n.child_by_field_name("body").is_some() {
            let pos = n.start_position();
            let line = pos.row as u32 + 1;
            let name = format!("<lambda@{line}>");
            let qname = format!("{}.{name}", ctx.module);
            let id = node_id(ctx.repo_id, "python", &ctx.relpath, &qname, Kind::Function);
            ctx.out.symbols.push(Symbol {
                id,
                qname,
                name,
                kind: Kind::Function,
                file: ctx.relpath.clone(),
                line,
                col: pos.column as u32,
                is_public: false,
                doc: "@lambda".into(),
                hash: blake3::hash(text(n, src).as_bytes()).to_hex()[..16].to_string(),
            });
        }
        for child in n.children(&mut cursor) {
            stack.push(child);
        }
    }
}
