//! Axum HTTP route detection — extends the Rust extractor.
//!
//! Patterns supported:
//! - `Router::new().route("/path", get(handler))` — single verb
//! - `Router::new().route("/path", get(h).post(p))` — chained `MethodRouter`
//!
//! ## Limitations
//!
//! - **`Router::new().nest("/api", inner)`** prefix tracking is NOT
//!   implemented end-to-end. The simple same-file case (`let inner =
//!   Router::new().route(...); .nest("/api", inner)`) is feasible but
//!   the realistic Axum pattern is `nest("/api", users::routes())`
//!   where `users::routes()` is a function in another module that
//!   builds and returns a `Router`. That requires inter-procedural
//!   flow analysis to know which routes belong to the returned
//!   Router — beyond what the per-file tree-sitter extractor does.
//!   FastAPI's `app.include_router(<var>, prefix=...)` works because
//!   the var is visible at the registration site; Axum's pattern
//!   pushes the Router through a function call boundary.
//!   Tracked as item #7 in the remaining-issues registry; consider
//!   re-tackling once a cross-fn def-chain analysis lands in
//!   `ckg-resolve`.
//! - **Combinator-style** `MethodRouter::new().get(h).post(p)` (no
//!   enclosing `route()` call) is not detected.

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

use crate::lang::rust::{Ctx, text};

const VERBS: &[&str] = &["get", "post", "put", "delete", "patch", "head", "options"];

pub(crate) fn detect_axum_route(ctx: &mut Ctx<'_>, n: Node<'_>, src: &str) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let mut path: Option<String> = None;
    let mut emissions: Vec<(String, String)> = Vec::new(); // (METHOD, handler)
    let mut cursor = args.walk();
    for arg in args.children(&mut cursor) {
        match arg.kind() {
            "string_literal" | "raw_string_literal" => {
                let inner = arg.child_by_field_name("content").or_else(|| {
                    let mut sc = arg.walk();
                    arg.children(&mut sc).find(|c| c.kind() == "string_content")
                });
                path = Some(match inner {
                    Some(c) => text(c, src).to_string(),
                    None => text(arg, src).trim_matches('"').to_string(),
                });
            }
            // Plain `get(handler)` — single verb.
            "call_expression" => {
                collect_axum_verb_call(arg, src, &mut emissions);
            }
            _ => {}
        }
    }
    let Some(p) = path else { return };
    if emissions.is_empty() {
        return;
    }
    let pos = n.start_position();
    for (m, h) in emissions {
        let qname = format!("{m} {p}");
        // Item #25: per-file `node_id` so duplicate `METHOD path` in
        // multiple files (admin server + main server with their own
        // `GET /healthz`) get distinct ids.
        let id = node_id(ctx.repo_id, "rust", &ctx.relpath, &qname, Kind::Route);
        ctx.out.symbols.push(Symbol {
            id,
            qname: qname.clone(),
            name: qname,
            kind: Kind::Route,
            file: ctx.relpath.clone(),
            line: pos.row as u32 + 1,
            col: pos.column as u32,
            is_public: true,
            doc: format!("handler={h}"),
            hash: blake3::hash(format!("{m}{p}{h}").as_bytes()).to_hex()[..16].to_string(),
        });
    }
}

/// Walk a `call_expression` node looking for axum verb calls and push
/// `(METHOD, handler)` tuples for each one found.
///
/// Two cases handled:
/// 1. **Plain**: `get(handler)` — `function` is an `identifier` matching
///    a verb; emit one tuple.
/// 2. **Chained `MethodRouter`**: `get(h).post(p).put(q)` — the
///    `call_expression` is `(get(h).post(p)).put(q)` (left-associative).
///    Recurse down the receiver chain harvesting verb calls until the
///    base `get(h)` is reached. Wave-8 fix for issue #6 (chained verbs
///    previously dropped after the first).
fn collect_axum_verb_call(call: Node<'_>, src: &str, out: &mut Vec<(String, String)>) {
    let Some(func) = call.child_by_field_name("function") else {
        return;
    };
    match func.kind() {
        // Plain: `get(handler)`.
        "identifier" | "scoped_identifier" => {
            let verb = text(func, src).to_string();
            // For scoped_identifier like `axum::routing::get`, take leaf.
            let verb_leaf = verb.rsplit("::").next().unwrap_or(&verb).to_string();
            if !VERBS.contains(&verb_leaf.as_str()) {
                return;
            }
            let handler = first_handler_arg(call, src).unwrap_or_default();
            out.push((verb_leaf.to_uppercase(), handler));
        }
        // Chained: `get(h).post(p)` — `function` is a `field_expression`
        // whose value is the prior call and `field` is the next verb.
        "field_expression" => {
            let Some(field) = func.child_by_field_name("field") else {
                return;
            };
            let verb = text(field, src).to_string();
            if VERBS.contains(&verb.as_str()) {
                let handler = first_handler_arg(call, src).unwrap_or_default();
                out.push((verb.to_uppercase(), handler));
            }
            // Recurse into the receiver to capture earlier verbs.
            if let Some(value) = func.child_by_field_name("value")
                && value.kind() == "call_expression"
            {
                collect_axum_verb_call(value, src, out);
            }
        }
        _ => {}
    }
}

/// Extract the first identifier-like argument from a verb call's
/// `arguments` node — that's the handler function name.
fn first_handler_arg(call: Node<'_>, src: &str) -> Option<String> {
    let inner_args = call.child_by_field_name("arguments")?;
    let mut ic = inner_args.walk();
    inner_args
        .children(&mut ic)
        .find(|ia| {
            matches!(
                ia.kind(),
                "identifier" | "scoped_identifier" | "field_expression"
            )
        })
        .map(|n| text(n, src).to_string())
}
