//! Rails route DSL — public entry point, verb routes, `mount`, `namespace`,
//! `scope`, `root`, `controller` blocks, `concern` definitions and
//! references, `direct`/`resolve` URL helpers.
//!
//! Submodules handle the larger logical sections:
//!   - `resources` — `resources`/`resource` extraction + `ActionFilter`
//!   - `implicit`  — `@implicit`-tagged controller-derived route emission

mod resources;
mod implicit;

pub(crate) use resources::collect_calls;
pub(crate) use implicit::emit_implicit_controller_routes;

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

use crate::lang::ruby::{Ctx, strip_quotes, text};

// ---------------------------------------------------------------------------
// Core route emitter
// ---------------------------------------------------------------------------

pub(super) fn push_route(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    method: &str,
    path_part: &str,
    handler: &str,
) {
    let pos = n.start_position();
    // Format-constraint split: `/posts.:format` → path `/posts`, format
    // param `:format`. Rails treats the trailing `.:<sym>` segment as a
    // format constraint, not part of the route path. Stripping it lets
    // canonical path matching work; recording it in the doc keeps the
    // information queryable.
    let (clean_path_part, format_param): (&str, Option<&str>) = match path_part.rfind(".:") {
        Some(i) => {
            let candidate = &path_part[i + 2..];
            // Only strip if the candidate is a clean identifier (no `/`).
            if !candidate.is_empty() && !candidate.contains('/') {
                (&path_part[..i], Some(candidate))
            } else {
                (path_part, None)
            }
        }
        None => (path_part, None),
    };
    let full_path = if clean_path_part.starts_with('/') {
        format!("{}{clean_path_part}", ctx.route_prefix)
    } else {
        format!("{}/{clean_path_part}", ctx.route_prefix)
    };
    let id = format!("{}:ruby:route:{method} {full_path}", ctx.repo_id);
    let doc = match format_param {
        Some(fmt) => format!("handler={handler} format=:{fmt}"),
        None => format!("handler={handler}"),
    };
    ctx.out.symbols.push(Symbol {
        id,
        qname: format!("{method} {full_path}"),
        name: format!("{method} {full_path}"),
        kind: Kind::Route,
        file: ctx.relpath.clone(),
        line: pos.row as u32 + 1,
        col: pos.column as u32,
        is_public: true,
        doc,
        hash: blake3::hash(format!("{method}{full_path}{handler}").as_bytes()).to_hex()[..16]
            .to_string(),
    });
}

// ---------------------------------------------------------------------------
// Route block entry — `Rails.application.routes.draw do … end`
// ---------------------------------------------------------------------------

pub(crate) fn enter_route_block(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    src: &str,
    scope: &str,
    apply_prefix: bool,
) {
    let mut prefix_seg: Option<String> = None;
    if apply_prefix && let Some(args) = n.child_by_field_name("arguments") {
        let mut c = args.walk();
        for arg in args.children(&mut c) {
            match arg.kind() {
                "simple_symbol" => {
                    prefix_seg = Some(strip_quotes(text(arg, src)).to_string());
                    break;
                }
                "string" => {
                    let s = strip_quotes(text(arg, src));
                    prefix_seg = Some(s.trim_start_matches('/').to_string());
                    break;
                }
                _ => {}
            }
        }
    }
    let prev = ctx.route_prefix.clone();
    if let Some(seg) = prefix_seg {
        ctx.route_prefix = format!("{prev}/{seg}");
    }
    let mut c = n.walk();
    for child in n.children(&mut c) {
        if child.kind() == "do_block" || child.kind() == "block" {
            crate::lang::ruby::walk(ctx, child, src, scope);
        }
    }
    ctx.route_prefix = prev;
}

// ---------------------------------------------------------------------------
// Main route DSL dispatcher
// ---------------------------------------------------------------------------

pub(crate) fn detect_rails_routes(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    src: &str,
    method: Option<&str>,
) {
    const VERBS: &[&str] = &["get", "post", "put", "patch", "delete", "match"];
    let Some(verb) = method else { return };

    if verb == "mount" {
        handle_mount(ctx, n, src);
        return;
    }

    if verb == "resources" || verb == "resource" {
        resources::handle_resources(ctx, n, src, verb);
        return;
    }

    if verb == "root" {
        handle_root(ctx, n, src);
        return;
    }

    if !VERBS.contains(&verb) {
        return;
    }

    handle_verb_route(ctx, n, src, verb);
}

// ---------------------------------------------------------------------------
// mount
// ---------------------------------------------------------------------------

fn handle_mount(ctx: &mut Ctx<'_>, n: Node<'_>, src: &str) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    // M5: traverse argument children one-by-one instead of splitting raw text
    // on '='/',' (which breaks for string paths containing those characters).
    let mut mount_engine = String::new();
    let mut mount_path = String::new();
    let mut ac = args.walk();
    for arg in args.children(&mut ac) {
        match arg.kind() {
            // `MyEngine` or `MyEngine::App` — the engine constant (first positional).
            "constant" | "scope_resolution" | "identifier" if mount_engine.is_empty() => {
                mount_engine = text(arg, src).to_string();
            }
            // Two forms:
            //   Hash rocket: `Sidekiq::Web => '/sidekiq'` — key is engine, value is path.
            //   Keyword arg: `at: "/path"` — key is `:at`, value is path.
            "pair" => {
                let key = arg.child_by_field_name("key");
                let val = arg.child_by_field_name("value");
                if let (Some(k), Some(v)) = (key, val) {
                    let key_text = text(k, src).trim_matches(':').trim().to_string();
                    if key_text == "at" {
                        // `at: "/path"` keyword form.
                        mount_path = text(v, src)
                            .trim_matches('"')
                            .trim_matches('\'')
                            .to_string();
                    } else if matches!(k.kind(), "constant" | "scope_resolution") {
                        // `Engine => "/path"` hash-rocket form: key is the engine.
                        if mount_engine.is_empty() {
                            mount_engine = key_text;
                        }
                        if mount_path.is_empty() {
                            mount_path = text(v, src)
                                .trim_matches('"')
                                .trim_matches('\'')
                                .to_string();
                        }
                    }
                }
            }
            // Positional string: `mount MyEngine, "/path"`.
            "string" | "string_literal" if mount_path.is_empty() => {
                if let Some(content) = arg.child_by_field_name("content").or_else(|| {
                    let mut sc = arg.walk();
                    arg.children(&mut sc).find(|c| c.kind() == "string_content")
                }) {
                    mount_path = text(content, src).to_string();
                } else {
                    mount_path = text(arg, src)
                        .trim_matches('"')
                        .trim_matches('\'')
                        .to_string();
                }
            }
            _ => {}
        }
    }
    if !mount_path.is_empty() {
        push_route(ctx, n, "MOUNT", &mount_path, &mount_engine);
    }
}

// ---------------------------------------------------------------------------
// root
// ---------------------------------------------------------------------------

fn handle_root(ctx: &mut Ctx<'_>, n: Node<'_>, src: &str) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let raw = text(args, src);
    if let Some(start) = raw.find('\'').or_else(|| raw.find('"')) {
        let rest = &raw[start + 1..];
        let end = rest
            .find('\'')
            .or_else(|| rest.find('"'))
            .unwrap_or(rest.len());
        push_route(ctx, n, "GET", "/", &rest[..end]);
    }
}

// ---------------------------------------------------------------------------
// Verb routes — get/post/put/patch/delete/match
// ---------------------------------------------------------------------------

fn handle_verb_route(ctx: &mut Ctx<'_>, n: Node<'_>, src: &str, verb: &str) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let mut path_part: Option<String> = None;
    let mut handler: Option<String> = None;
    let mut named_as: Option<String> = None;
    let mut explicit_ctrl: Option<String> = None;
    let mut explicit_action: Option<String> = None;
    let process_pair = |kv: Node<'_>,
                        ctx_src: &str,
                        path_part: &mut Option<String>,
                        handler: &mut Option<String>,
                        named_as: &mut Option<String>,
                        explicit_ctrl: &mut Option<String>,
                        explicit_action: &mut Option<String>| {
        if kv.kind() != "pair" {
            return;
        }
        let key = kv.child_by_field_name("key").map(|k| text(k, ctx_src));
        let val_node = kv.child_by_field_name("value");
        let val = val_node.map(|v| text(v, ctx_src));
        let (Some(k), Some(v)) = (key, val) else {
            return;
        };
        let k_clean = strip_quotes(k.trim_end_matches(':'));
        // `to: redirect('/new')` — preserve `redirect:/new` as the handler so
        // consumers can distinguish redirects from controller actions.
        let v_clean = if let Some(node) = val_node
            && node.kind() == "call"
            && node.child_by_field_name("method").map(|m| text(m, ctx_src)) == Some("redirect")
        {
            // Extract the first quoted target inside `redirect(...)`.
            let raw = text(node, ctx_src);
            if let Some(start) = raw.find('\'').or_else(|| raw.find('"')) {
                let rest = &raw[start + 1..];
                let end = rest
                    .find('\'')
                    .or_else(|| rest.find('"'))
                    .unwrap_or(rest.len());
                format!("redirect:{}", &rest[..end])
            } else {
                strip_quotes(v).to_string()
            }
        } else {
            strip_quotes(v).to_string()
        };
        if k_clean.starts_with('/') && path_part.is_none() {
            *path_part = Some(k_clean.to_string());
            *handler = Some(v_clean);
            return;
        }
        match k_clean {
            "to" => *handler = Some(v_clean),
            "as" => *named_as = Some(v_clean),
            "controller" => *explicit_ctrl = Some(v_clean),
            "action" => *explicit_action = Some(v_clean),
            _ => {}
        }
    };
    let mut c = args.walk();
    for arg in args.children(&mut c) {
        match arg.kind() {
            "string" if path_part.is_none() => {
                path_part = Some(strip_quotes(text(arg, src)).to_string());
            }
            "pair" => process_pair(
                arg,
                src,
                &mut path_part,
                &mut handler,
                &mut named_as,
                &mut explicit_ctrl,
                &mut explicit_action,
            ),
            "hash" => {
                let mut hc = arg.walk();
                for kv in arg.children(&mut hc) {
                    process_pair(
                        kv,
                        src,
                        &mut path_part,
                        &mut handler,
                        &mut named_as,
                        &mut explicit_ctrl,
                        &mut explicit_action,
                    );
                }
            }
            _ => {}
        }
    }
    if handler.is_none()
        && let (Some(c), Some(a)) = (&explicit_ctrl, &explicit_action)
    {
        handler = Some(format!("{c}#{a}"));
    }
    if let Some(p) = path_part {
        let mut h = handler.unwrap_or_default();
        if let Some(name) = named_as {
            h = if h.is_empty() {
                format!("as={name}")
            } else {
                format!("{h} as={name}")
            };
        }
        push_route(ctx, n, &verb.to_uppercase(), &p, &h);
    }
}

// ---------------------------------------------------------------------------
// Controller block — `controller :foos do; get :show; end`
// ---------------------------------------------------------------------------

/// `controller :foos do; get :show; end` (Rails 5+ routes). Path prefix is
/// `/foos`; routes inside use `foos#<action>` as handler.
pub(crate) fn enter_controller_block(ctx: &mut Ctx<'_>, n: Node<'_>, src: &str, scope: &str) {
    let mut controller_name: Option<String> = None;
    if let Some(args) = n.child_by_field_name("arguments") {
        let mut c = args.walk();
        for arg in args.children(&mut c) {
            if arg.kind() == "simple_symbol" {
                controller_name = Some(strip_quotes(text(arg, src)).to_string());
                break;
            }
        }
    }
    let Some(name) = controller_name else { return };
    let prev_prefix = ctx.route_prefix.clone();
    ctx.route_prefix = format!("{}/{name}", prev_prefix);
    let mut c = n.walk();
    for child in n.children(&mut c) {
        if child.kind() != "do_block" && child.kind() != "block" {
            continue;
        }
        // Collect verb calls within the block.
        let mut calls: Vec<Node<'_>> = Vec::new();
        collect_calls(child, &mut calls);
        for call in calls {
            let m = call.child_by_field_name("method").map(|x| text(x, src));
            let Some(verb) = m else { continue };
            if !matches!(verb, "get" | "post" | "put" | "patch" | "delete") {
                continue;
            }
            let Some(args) = call.child_by_field_name("arguments") else {
                continue;
            };
            let mut ac = args.walk();
            for a in args.children(&mut ac) {
                if a.kind() == "simple_symbol" {
                    let action = strip_quotes(text(a, src));
                    let path = format!("/{action}");
                    let handler = format!("{name}#{action}");
                    push_route(ctx, call, &verb.to_uppercase(), &path, &handler);
                    break;
                }
            }
        }
        let _ = scope;
    }
    ctx.route_prefix = prev_prefix;
}

// ---------------------------------------------------------------------------
// concern / concerns / direct / resolve / mailer routes
// ---------------------------------------------------------------------------

/// `concern :commentable do … end` (Rails 5+ routes) — emit a Module-kind
/// Symbol at qname `concerns.<name>` so the concern can be searched/linked,
/// and recurse into the body for any verb-routes defined in it (rare but
/// valid). Body recursion uses an empty class_id since we're in routes scope.
pub(crate) fn handle_concern_definition(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    src: &str,
    scope: &str,
) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let mut name: Option<String> = None;
    let mut c = args.walk();
    for arg in args.children(&mut c) {
        if arg.kind() == "simple_symbol" {
            name = Some(strip_quotes(text(arg, src)).to_string());
            break;
        }
    }
    let Some(name) = name else { return };
    let qname = format!("concerns.{name}");
    let id = ctx.push(n, src, Kind::Module, qname, name.clone());
    let _ = id;
    // Recurse into the do_block to capture inner DSL (resources, gets, etc.).
    if let Some(block) = n.child_by_field_name("block").or_else(|| {
        let mut c = n.walk();
        n.children(&mut c)
            .find(|n| n.kind() == "do_block" || n.kind() == "block")
    }) {
        crate::lang::ruby::walk(ctx, block, src, scope);
    }
}

/// `concerns :commentable` / `concerns :commentable, :taggable` — emit a
/// Calls edge from the **caller routes container** to each referenced
/// concern. `caller_id` is best-effort the file's module id.
pub(crate) fn handle_concern_reference(ctx: &mut Ctx<'_>, n: Node<'_>, src: &str) {
    use ckg_core::{Edge, EdgeKind};
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let mut c = args.walk();
    for arg in args.children(&mut c) {
        if arg.kind() == "simple_symbol" {
            let name = strip_quotes(text(arg, src)).to_string();
            ctx.out.edges.push(Edge {
                kind: EdgeKind::Calls,
                src: ctx.module.clone(),
                dst: format!("concerns.{name}"),
                confidence: 0.9,
            });
        }
    }
}

/// `direct :browse do |…| end` (Rails 5+) — emit a `<name>_url` Method
/// Symbol. Rails generates this URL helper at runtime.
pub(crate) fn handle_direct_url_helper(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    src: &str,
    scope: &str,
) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let mut c = args.walk();
    for arg in args.children(&mut c) {
        if arg.kind() == "simple_symbol" {
            let raw = strip_quotes(text(arg, src)).to_string();
            let name = format!("{raw}_url");
            let qname = if scope.is_empty() {
                format!("{}.{name}", ctx.module)
            } else {
                format!("{}::{scope}.{name}", ctx.module)
            };
            let id = ctx.push(n, src, Kind::Method, qname, name);
            let _ = id;
            return;
        }
    }
}

/// `resolve('Foo') { |f| f }` (Rails 5+) — emit a `<class>_url` Method.
pub(crate) fn handle_resolve_url_helper(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    src: &str,
    scope: &str,
) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let mut c = args.walk();
    for arg in args.children(&mut c) {
        if arg.kind() == "string" {
            let raw = strip_quotes(text(arg, src)).to_lowercase();
            let name = format!("{raw}_url");
            let qname = if scope.is_empty() {
                format!("{}.{name}", ctx.module)
            } else {
                format!("{}::{scope}.{name}", ctx.module)
            };
            ctx.push(n, src, Kind::Method, qname, name);
            return;
        }
    }
}
