//! JavaScript / TypeScript / TSX extractor.
//!
//! Uses three tree-sitter grammars — `tree-sitter-javascript`,
//! `tree-sitter-typescript::language_typescript()`, and
//! `tree-sitter-typescript::language_tsx()` — sharing one walker since
//! TS is mostly a superset of JS at the parser level. Differences
//! that matter for our purposes:
//!   - TS adds `interface_declaration`, `type_alias_declaration`,
//!     `enum_declaration`, typed parameters, return types
//!   - TSX adds JSX expressions (we ignore JSX-specific element
//!     traversal; JSX call patterns still reach `call_expression`)
//!
//! Symbol kinds emitted: Function, Class, Method, Constant (top-level
//! `const`/`let`), Interface, TypeAlias, Enum.
//! Edges: Imports (from `import` / `import * as`), Calls (text-form,
//! confidence 0.5/1.0), Extends (class inheritance + interface extends).

pub(crate) 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;
use crate::util::text;

/// Strip extension and join path segments with `.` so qnames look like
/// `src.api.users` for `src/api/users.ts`.
fn module_name(path: &Path) -> String {
    path.with_extension("")
        .components()
        .filter_map(|c| c.as_os_str().to_str().map(str::to_owned))
        .collect::<Vec<_>>()
        .join(".")
}

pub(crate) struct Ctx<'a> {
    pub(crate) repo_id: &'a str,
    pub(crate) lang: &'a str,
    pub(crate) relpath: String,
    pub(crate) module: String,
    pub(crate) out: ParseOutput,
    pub(crate) name_to_id: std::collections::HashMap<String, String>,
    /// Import alias → fully-qualified path. `import { foo as bar } from
    /// 'lib'` → `bar` → `lib.foo`. `import * as x from 'lib'` → `x` → `lib`.
    pub(crate) aliases: std::collections::HashMap<String, String>,
    /// Variable name → type name. Populated by:
    /// `const x: T = …` (TS annotation), `const x = new T()` (ctor pattern),
    /// and `(x: T) => …` / `function f(x: T) {…}` (TS-typed params).
    /// Used to upgrade `x.method()` calls from leaf-only conf 0.5 to
    /// `T.method` at conf 0.7.
    pub(crate) types: std::collections::HashMap<String, String>,
    /// Inside `class Type { method() { … } }`, the `Type` name. Lets
    /// `this.foo()` resolve to `Type.foo` at conf 0.7. Mirrors Rust's
    /// `Ctx.impl_self_type`.
    pub(crate) this_type: Option<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, self.lang, &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,
            is_public: true, // JS/TS has `export` at the statement level — treat all as public for now.
            doc,
            // CR-parse-H-1: compute body hash like Rust/Python/Ruby do.
            // Pre-fix this was String::new() so JS/TS/Go file-staleness
            // queries against `Symbol.hash` always fired (every symbol
            // looked changed every run).
            hash: blake3::hash(text(n, src).as_bytes()).to_hex()[..16].to_string(),
        });
        id
    }
}

pub(crate) fn name_of_field<'a>(n: Node<'a>, src: &str) -> Option<String> {
    n.child_by_field_name("name")
        .map(|x| text(x, src).to_string())
}

/// Collect a leading JSDoc `/** … */` comment if present immediately
/// before the node.
pub(crate) fn collect_jsdoc(n: Node<'_>, src: &str) -> String {
    let Some(prev) = n.prev_sibling() else {
        return String::new();
    };
    if prev.kind() != "comment" {
        return String::new();
    }
    let body = text(prev, src);
    if !body.starts_with("/**") {
        return String::new();
    }
    body.lines()
        .map(|l| {
            l.trim()
                .trim_start_matches('*')
                .trim_start_matches('/')
                .trim()
        })
        .filter(|s| !s.is_empty() && *s != "*")
        .collect::<Vec<_>>()
        .join("\n")
}

pub(crate) fn qualify(module: &str, scope: &str, name: &str) -> String {
    match (module, scope) {
        ("", "") => name.to_string(),
        ("", s) => format!("{s}.{name}"),
        (m, "") => format!("{m}.{name}"),
        (m, s) => format!("{m}.{s}.{name}"),
    }
}

/// First descendant (depth-first) whose `kind()` matches.
pub(crate) fn first_named_child<'a>(n: Node<'a>, kind: &str) -> Option<Node<'a>> {
    let mut stack = vec![n];
    while let Some(node) = stack.pop() {
        if node.kind() == kind {
            return Some(node);
        }
        let mut c = node.walk();
        for ch in node.children(&mut c) {
            stack.push(ch);
        }
    }
    None
}

/// Harvest `(x: T, y: U)` parameter types into the type map. TS-only
/// for the actual TS grammar; on JS this is a no-op since `formal_parameters`
/// children are bare `identifier`s without type annotations.
pub(crate) fn collect_param_types(
    params: Node<'_>,
    src: &str,
    out: &mut std::collections::HashMap<String, String>,
) {
    let mut c = params.walk();
    for p in params.children(&mut c) {
        // TS: `required_parameter` / `optional_parameter`. JS: `identifier`.
        if !matches!(
            p.kind(),
            "required_parameter" | "optional_parameter" | "parameter"
        ) {
            continue;
        }
        let mut pc = p.walk();
        let mut name: Option<String> = None;
        let mut ty: Option<String> = None;
        for ch in p.children(&mut pc) {
            match ch.kind() {
                "identifier" if name.is_none() => name = Some(text(ch, src).to_string()),
                "type_annotation" => {
                    let mut tc = ch.walk();
                    if let Some(t) = ch.children(&mut tc).find(|x| {
                        matches!(
                            x.kind(),
                            "type_identifier" | "predefined_type" | "generic_type"
                        )
                    }) {
                        let raw = text(t, src);
                        ty = Some(raw.split('<').next().unwrap_or(raw).trim().to_string());
                    }
                }
                _ => {}
            }
        }
        if let (Some(n), Some(t)) = (name, ty) {
            out.insert(n, t);
        }
    }
}

/// Collect TS decorator names attached to a class: `@Comp class X {}` → `["Comp"]`.
pub(crate) fn collect_decorators(class_node: Node<'_>, src: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut c = class_node.walk();
    for ch in class_node.children(&mut c) {
        if ch.kind() != "decorator" {
            continue;
        }
        // Decorator child structure: `@` then identifier or call_expression.
        let mut dc = ch.walk();
        for d in ch.children(&mut dc) {
            match d.kind() {
                "identifier" => out.push(text(d, src).to_string()),
                "call_expression" => {
                    if let Some(f) = d.child_by_field_name("function") {
                        out.push(text(f, src).to_string());
                    }
                }
                _ => {}
            }
        }
    }
    out
}

/// Walk a `class_heritage` looking for `implements_clause` and collect
/// each interface name into `out`. TS-only.
pub(crate) fn collect_implements(heritage: Node<'_>, src: &str, out: &mut Vec<String>) {
    let mut c = heritage.walk();
    for ch in heritage.children(&mut c) {
        if ch.kind() != "implements_clause" {
            continue;
        }
        let mut ic = ch.walk();
        for inner in ch.children(&mut ic) {
            if matches!(
                inner.kind(),
                "type_identifier" | "identifier" | "generic_type"
            ) {
                let raw = text(inner, src);
                out.push(raw.split('<').next().unwrap_or(raw).trim().to_string());
            }
        }
    }
}

/// Stable, qname-friendly slug from a free-form test description.
/// Convert a test description string to a valid identifier fragment.
///
/// L5: uses `char::is_alphanumeric()` (Unicode-aware) instead of
/// `is_ascii_alphanumeric()` so non-ASCII description text (e.g. Japanese,
/// accented Latin) is preserved rather than collapsed to underscores.
pub(crate) fn slugify(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let mut last_underscore = false;
    for c in s.chars() {
        if c.is_alphanumeric() {
            out.push(c);
            last_underscore = false;
        } else if !last_underscore {
            out.push('_');
            last_underscore = true;
        }
    }
    out.trim_matches('_').to_string()
}

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

/// Entry point. `lang` is one of `"javascript"`, `"typescript"`, `"tsx"`.
/// Selects the matching tree-sitter grammar.
pub fn extract(repo_id: &str, path: &Path, source: &str, lang: &str) -> Result<ParseOutput> {
    let language: tree_sitter::Language = match lang {
        "javascript" => tree_sitter_javascript::LANGUAGE.into(),
        "typescript" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
        "tsx" => tree_sitter_typescript::LANGUAGE_TSX.into(),
        other => {
            return Err(ckg_core::Error::UnsupportedLanguage(other.to_string()));
        }
    };
    // PARSE-H9: pool key must be a &'static str; map at compile time.
    let pool_key: &'static str = match lang {
        "javascript" => "javascript",
        "typescript" => "typescript",
        "tsx" => "tsx",
        _ => "javascript", // unreachable — UnsupportedLanguage returned above
    };
    let mut parser = crate::util::get_pooled_parser(pool_key, &language);
    let tree = crate::util::guarded_parse(&mut parser, source, lang);
    // 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,
        lang,
        relpath: crate::util::path_to_relpath(path)?,
        module: module_name(path),
        out: ParseOutput::default(),
        name_to_id: std::collections::HashMap::new(),
        aliases: std::collections::HashMap::new(),
        types: std::collections::HashMap::new(),
        this_type: None,
    };
    walk::walk(&mut ctx, tree.root_node(), "", source);
    crate::util::return_pooled_parser(pool_key, parser);
    Ok(ctx.out)
}

#[cfg(test)]
mod tests;
