//! Rails `resources`/`resource` route extraction — `ActionFilter`, argument
//! parsing, member/collection nested routes, and the `@resources`-tagged
//! route emitter.

use std::collections::HashSet;

use tree_sitter::Node;

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

// ---------------------------------------------------------------------------
// ActionFilter
// ---------------------------------------------------------------------------

/// `Only(set)` / `Except(set)` / `All` — the action filter declared on a
/// `resources :foo, only: [:show, :index]` line.
///
/// Used by the routes.rb resources/resource extractor to suppress
/// emission of standard actions that the user explicitly excluded.
pub(crate) enum ActionFilter {
    All,
    Only(HashSet<String>),
    Except(HashSet<String>),
}

impl ActionFilter {
    pub(crate) fn allows(&self, action: &str) -> bool {
        match self {
            Self::All => true,
            Self::Only(set) => set.contains(action),
            Self::Except(set) => !set.contains(action),
        }
    }
}

// ---------------------------------------------------------------------------
// resources / resource handling
// ---------------------------------------------------------------------------

/// `resources :posts` / `resource :profile` — emit standard REST routes
/// respecting `:only` / `:except` filters, then recurse into nested blocks
/// for `member` and `collection` sub-routes.
pub(crate) fn handle_resources(ctx: &mut Ctx<'_>, n: Node<'_>, src: &str, verb: &str) {
    let Some(args) = n.child_by_field_name("arguments") else {
        return;
    };
    let (resource_names, filter) = parse_resources_args(args, src);
    if resource_names.is_empty() {
        return;
    }
    let standard: &[(&str, &str, &str)] = if verb == "resources" {
        &[
            ("GET", "", "index"),
            ("GET", "/new", "new"),
            ("POST", "", "create"),
            ("GET", "/:id", "show"),
            ("GET", "/:id/edit", "edit"),
            ("PATCH", "/:id", "update"),
            ("DELETE", "/:id", "destroy"),
        ]
    } else {
        &[
            ("GET", "/new", "new"),
            ("POST", "", "create"),
            ("GET", "", "show"),
            ("GET", "/edit", "edit"),
            ("PATCH", "", "update"),
            ("DELETE", "", "destroy"),
        ]
    };
    // AD6: respect `:only` / `:except` so `resources :posts, only: [:index]`
    // emits only `GET /posts`. The same filter applies to every name when the
    // multi-resource shorthand (`resources :posts, :tags`) is used.
    //
    // Each emitted route is tagged `@resources` in the doc field — AD6 dedup
    // uses this marker (not the absence of `@implicit`) to decide when a
    // resource is comprehensively declared. A bare `get '/posts/foo', to:
    // 'posts#foo'` does NOT tag `@resources`, so it can't trigger pruning of
    // standard implicit controller routes.
    for name in &resource_names {
        for (m, suffix, action) in standard {
            if !filter.allows(action) {
                continue;
            }
            let path = format!("/{name}{suffix}");
            let handler = format!("{name}#{action}");
            push_resources_route(ctx, n, m, &path, &handler);
        }
        detect_member_collection(ctx, n, src, name);
    }
}

// ---------------------------------------------------------------------------
// Argument parsing helpers
// ---------------------------------------------------------------------------

/// Parse the arguments of `resources :foo, :bar, only: [:show], except: [...]`.
/// Returns `(resource_names, filter)` — supports the multi-resource
/// shorthand `resources :a, :b, :c` (all share the same filter), which
/// the prior implementation silently dropped (only the first resource
/// emitted routes; subsequent resources were missed AND, because
/// wave-4's `@resources` tag never got attached to them, AD6 dedup
/// asymmetrically pruned only the first resource's controller routes).
///
/// Filter is `All` if no `only:` / `except:` keyword is present.
/// `only:` / `except:` are mutually exclusive in Rails; if both appear
/// (operator error) we treat `only:` as authoritative since it's
/// strictly more restrictive.
fn parse_resources_args(args: Node<'_>, src: &str) -> (Vec<String>, ActionFilter) {
    let mut resource_names: Vec<String> = Vec::new();
    let mut only: Option<HashSet<String>> = None;
    let mut except: Option<HashSet<String>> = None;
    let mut c = args.walk();
    for arg in args.children(&mut c) {
        match arg.kind() {
            "simple_symbol" => {
                resource_names.push(strip_quotes(text(arg, src)).to_string());
            }
            "pair" => collect_only_except(arg, src, &mut only, &mut except),
            "hash" => {
                let mut hc = arg.walk();
                for kv in arg.children(&mut hc) {
                    if kv.kind() == "pair" {
                        collect_only_except(kv, src, &mut only, &mut except);
                    }
                }
            }
            _ => {}
        }
    }
    let filter = match (only, except) {
        (Some(o), _) => ActionFilter::Only(o),
        (None, Some(e)) => ActionFilter::Except(e),
        (None, None) => ActionFilter::All,
    };
    (resource_names, filter)
}

fn collect_only_except(
    pair: Node<'_>,
    src: &str,
    only: &mut Option<HashSet<String>>,
    except: &mut Option<HashSet<String>>,
) {
    let key = pair.child_by_field_name("key").map(|k| text(k, src));
    let value = pair.child_by_field_name("value");
    let (Some(k), Some(v)) = (key, value) else {
        return;
    };
    let k_clean = strip_quotes(k.trim_end_matches(':'));
    if k_clean != "only" && k_clean != "except" {
        return;
    }
    let actions = collect_symbol_set(v, src);
    if k_clean == "only" {
        *only = Some(actions);
    } else {
        *except = Some(actions);
    }
}

/// Walk a value node and gather every `simple_symbol` text into a set
/// (sans the leading `:`). Handles both `:show` (single symbol) and
/// `[:show, :index]` (array literal).
///
/// M11 (PARSE-C1 family): implemented iteratively to avoid stack overflow on
/// deeply nested array literals.
fn collect_symbol_set(node: Node<'_>, src: &str) -> HashSet<String> {
    let mut set = HashSet::new();
    let mut stack: Vec<Node<'_>> = vec![node];
    while let Some(n) = stack.pop() {
        if n.kind() == "simple_symbol" {
            let raw = strip_quotes(text(n, src));
            let cleaned = raw.trim_start_matches(':').to_string();
            if !cleaned.is_empty() {
                set.insert(cleaned);
            }
            continue;
        }
        let mut c = n.walk();
        for child in n.children(&mut c) {
            stack.push(child);
        }
    }
    set
}

// ---------------------------------------------------------------------------
// Member / collection nested routes
// ---------------------------------------------------------------------------

pub(super) fn detect_member_collection(
    ctx: &mut Ctx<'_>,
    resources_node: Node<'_>,
    src: &str,
    name: &str,
) {
    let block: Option<Node<'_>> = resources_node.child_by_field_name("block").or_else(|| {
        let mut c = resources_node.walk();
        resources_node
            .children(&mut c)
            .find(|n| n.kind() == "do_block" || n.kind() == "block")
    });
    let Some(block) = block else { return };
    let mut calls: Vec<Node<'_>> = Vec::new();
    collect_calls(block, &mut calls);
    for call in calls {
        let m = call.child_by_field_name("method").map(|x| text(x, src));
        let scope_kind = match m {
            Some("member") => "/:id",
            Some("collection") => "",
            _ => continue,
        };
        let inner_block = call.child_by_field_name("block").or_else(|| {
            let mut c = call.walk();
            call.children(&mut c)
                .find(|n| n.kind() == "do_block" || n.kind() == "block")
        });
        let Some(inner) = inner_block else { continue };
        let mut verb_calls: Vec<Node<'_>> = Vec::new();
        collect_calls(inner, &mut verb_calls);
        for verb_call in verb_calls {
            let vm = verb_call
                .child_by_field_name("method")
                .map(|x| text(x, src));
            let Some(vm) = vm else { continue };
            if !matches!(vm, "get" | "post" | "put" | "patch" | "delete") {
                continue;
            }
            let Some(args) = verb_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!("/{name}{scope_kind}/{action}");
                    let handler = format!("{name}#{action}");
                    push_route(ctx, verb_call, &vm.to_uppercase(), &path, &handler);
                    break;
                }
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Shared tree-walking helper (used by member/collection + controller block)
// ---------------------------------------------------------------------------

pub(crate) fn collect_calls<'a>(node: Node<'a>, out: &mut Vec<Node<'a>>) {
    let mut c = node.walk();
    for child in node.children(&mut c) {
        if child.kind() == "call" {
            out.push(child);
        } else if matches!(child.kind(), "body_statement" | "do_block" | "block") {
            collect_calls(child, out);
        }
    }
}

// ---------------------------------------------------------------------------
// @resources-tagged route emitter
// ---------------------------------------------------------------------------

/// Push a route emitted from a `resources :foo` (or `resource :foo`)
/// declaration in routes.rb. Tagged `@resources` so the AD6 dedup pass
/// can distinguish comprehensive resource declarations from one-off
/// custom routes (e.g. `get '/posts/foo', to: 'posts#foo'`). Only
/// `@resources` tags trigger pruning of `@implicit` controller routes;
/// a single custom route doesn't.
fn push_resources_route(ctx: &mut Ctx<'_>, n: Node<'_>, method: &str, path: &str, handler: &str) {
    push_route(ctx, n, method, path, handler);
    if let Some(last) = ctx.out.symbols.last_mut() {
        last.doc = format!("@resources {}", last.doc);
    }
}
