//! Rails framework — extends the Ruby extractor with the full Rails DSL:
//! `routes.rb` (resources / namespace / scope / member / collection /
//! mount / constraints / verb routes), associations (`has_many`,
//! `belongs_to`, `has_one`, `habtm`), callbacks (`before_save` etc.),
//! validations (`validates`), named scopes, ActiveSupport::Concern
//! `included do`, and **implicit controller actions** (every method in
//! a class ending `Controller` becomes an implicit Route).

use tree_sitter::Node;

use crate::lang::ruby::Ctx;

pub(crate) mod handlers;

// Explicit re-export so external callers (lang/ruby.rs) reach handlers
// through `crate::framework::rails::<fn>` paths. Naming each item (vs
// glob `pub use handlers::*`) keeps file-private helpers (`collect_calls`,
// `capitalise`, `enclosing_class_actions`) out of the crate-public
// surface and gives static-graph tools a single canonical resolution
// per symbol.
pub(crate) use handlers::{
    class_framework_role, detect_rails_routes, emit_implicit_controller_routes, emit_mailer_routes,
    enter_controller_block, enter_route_block, handle_association, handle_attachment,
    handle_callback, handle_concern_definition, handle_concern_reference, handle_default_scope,
    handle_direct_url_helper, handle_field_macro, handle_mailbox_routing,
    handle_resolve_url_helper, handle_scope, handle_validation,
};

/// Single dispatch entry point for every Rails-specific call form.
///
/// Returns `true` if the call was Rails-specific and was handled — the
/// caller should NOT recurse or fall through. Returns `false` if the
/// call is not a Rails idiom (Ruby-native, or the verb-route fallback
/// is needed). Ruby-native idioms (`refine`, `define_method`, `included`,
/// mixins, `attr_*`, `alias_method`) stay in `lang/ruby.rs`.
pub(crate) fn dispatch_call(
    ctx: &mut Ctx<'_>,
    n: Node<'_>,
    src: &str,
    method: Option<&str>,
    class_id: Option<&str>,
    scope: &str,
    is_public: bool,
) -> bool {
    // routes.rb DSL (top-level only — class_id is None inside `routes.draw`).
    if class_id.is_none()
        && matches!(
            method,
            Some("namespace") | Some("scope") | Some("constraints")
        )
    {
        let prefix_only = method != Some("constraints");
        enter_route_block(ctx, n, src, scope, prefix_only);
        return true;
    }
    if class_id.is_none() && method == Some("controller") {
        enter_controller_block(ctx, n, src, scope);
        return true;
    }
    if class_id.is_none() && method == Some("concern") {
        handle_concern_definition(ctx, n, src, scope);
        return true;
    }
    if class_id.is_none() && method == Some("concerns") {
        handle_concern_reference(ctx, n, src);
        return true;
    }
    if class_id.is_none() && method == Some("direct") {
        handle_direct_url_helper(ctx, n, src, scope);
        return true;
    }
    if class_id.is_none() && method == Some("resolve") {
        handle_resolve_url_helper(ctx, n, src, scope);
        return true;
    }

    // Class-body Rails DSL (class_id is Some).
    let Some(cid) = class_id else {
        // Verb routes (`get`, `post`, `mount`, `root`, `resources`, …)
        // fire at the routes-DSL top level only. detect_rails_routes
        // is a no-op for non-route verbs, so calling it unconditionally
        // is fine — but we still return false so the caller recurses.
        detect_rails_routes(ctx, n, src, method);
        return false;
    };

    // CR-M-5: pattern-bind `method` so a future refactor that breaks the
    // matches!-then-unwrap pairing can't panic on first-malformed input.
    if let Some(m) = method
        && matches!(
            m,
            "has_many" | "has_one" | "belongs_to" | "has_and_belongs_to_many"
        )
    {
        handle_association(ctx, n, src, m, cid);
        return true;
    }
    if let Some(m) = method
        && matches!(
            m,
            "before_save"
                | "after_save"
                | "before_create"
                | "after_create"
                | "before_update"
                | "after_update"
                | "before_destroy"
                | "after_destroy"
                | "before_validation"
                | "after_validation"
                | "before_action"
                | "after_action"
                | "around_action"
        )
    {
        handle_callback(ctx, n, src, m, cid, scope);
        return true;
    }
    if method == Some("validates") {
        handle_validation(ctx, n, src, scope, cid);
        return true;
    }
    if method == Some("scope") {
        handle_scope(ctx, n, src, scope, cid, is_public);
        return true;
    }
    if method == Some("default_scope") {
        handle_default_scope(ctx, n, src, scope, cid, is_public);
        return true;
    }
    if let Some(m) = method
        && matches!(m, "has_one_attached" | "has_many_attached")
    {
        handle_attachment(ctx, n, src, m, scope, cid, is_public);
        return true;
    }
    if let Some(m) = method
        && matches!(m, "encrypts" | "normalizes")
    {
        handle_field_macro(ctx, n, src, m, scope, cid);
        return true;
    }
    if method == Some("routing") && scope.ends_with("Mailbox") {
        handle_mailbox_routing(ctx, n, src, scope);
        return true;
    }

    // Not a Rails class-DSL form — let the caller try Ruby-native handling
    // and recurse. Verb routes still fire (no-op when method isn't a verb).
    detect_rails_routes(ctx, n, src, method);
    false
}

/// Class-level Rails hooks fired right after `class Foo < Bar` has been
/// pushed: stamp framework role on the doc, emit implicit controller
/// routes, emit mailer routes.
pub(crate) fn class_hooks(
    ctx: &mut Ctx<'_>,
    class_name: &str,
    class_id: &str,
    superclass: &str,
    body: Node<'_>,
    src: &str,
) {
    if let Some(role) = class_framework_role(superclass)
        && let Some(sym) = ctx.symbol_by_id_mut(class_id)
    {
        sym.doc = role.to_string();
    }
    if class_name.ends_with("Controller") && class_name.len() > "Controller".len() {
        emit_implicit_controller_routes(ctx, body, src, class_name);
    }
    if class_name.ends_with("Mailer") && class_name.len() > "Mailer".len() {
        emit_mailer_routes(ctx, body, src, class_name);
    }
}

/// `PostsController` → `posts`; `BlogPostsController` → `blog_posts`.
///
/// **L11 limitation**: this is a heuristic pluralizer, not a full English
/// inflector. Known false cases: `PersonController` → `persons` (not `people`),
/// `MouseController` → `mouses` (not `mice`). For correctness on irregular
/// plurals, consider replacing with the `inflector` crate.
pub(crate) fn pluralize_snake(s: &str) -> String {
    let mut snake = String::with_capacity(s.len() + 4);
    for (i, c) in s.chars().enumerate() {
        if i > 0 && c.is_ascii_uppercase() {
            snake.push('_');
        }
        snake.extend(c.to_lowercase());
    }
    if snake.ends_with('s') {
        return snake;
    }
    if snake.ends_with('y') && snake.len() > 1 {
        let mut t = snake[..snake.len() - 1].to_string();
        t.push_str("ies");
        return t;
    }
    if snake.ends_with("ch") || snake.ends_with("sh") {
        return format!("{snake}es");
    }
    format!("{snake}s")
}
