//! Ruby extractor.
//!
//! Tree-sitter walk emitting Symbols (class/module/method/constant) plus
//! Calls/Imports/Extends/Defines edges. Pure-Ruby idioms only — `include`,
//! `extend`, `attr_*`, `alias_method`, `define_method`, `class << self`,
//! refinements, `Struct.new`, ActiveSupport-style `included do`. All
//! Rails-specific DSL (routes, callbacks, validations, associations,
//! mailers, mailboxes) lives in `crate::framework::rails` and is reached
//! through two entry points: `rails::dispatch_call` for inline DSL calls
//! and `rails::class_hooks` for class-body framework hooks (controller
//! routes, mailer routes, framework role tags).

mod calls;
mod walk;

use std::path::Path;

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

use crate::ParseOutput;

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

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

pub(crate) struct Ctx<'a> {
    pub(crate) repo_id: &'a str,
    pub(crate) relpath: String,
    pub(crate) module: String,
    pub(crate) out: ParseOutput,
    /// Current Rails route path prefix from enclosing `namespace`/`scope` blocks.
    pub(crate) route_prefix: String,
    /// Inside `refine T do … end`, the refinement-target type name. Methods
    /// emitted while this is `Some` get a `@refinement <T>` doc tag.
    pub(crate) refinement_target: Option<String>,
    /// CR-parse-H-3: O(1) `id → out.symbols` index. Pre-fix the post-emit
    /// doc-tagging pattern at walk.rs:96/113/484 + rails.rs:166 did
    /// `out.symbols.iter_mut().find(|s| s.id == id)` — O(n²) total over
    /// a file. Populated by every push.
    pub(crate) id_to_idx: std::collections::HashMap<String, usize>,
}

impl Ctx<'_> {
    /// CR-parse-H-3: O(1) lookup helper for the post-emit doc-tagging
    /// pattern. Returns the just-pushed (or earlier-pushed) symbol so
    /// callers can amend its `doc` field without an O(n) scan.
    pub(crate) fn symbol_by_id_mut(&mut self, id: &str) -> Option<&mut ckg_core::Symbol> {
        let idx = *self.id_to_idx.get(id)?;
        self.out.symbols.get_mut(idx)
    }
}

impl Ctx<'_> {
    pub(crate) fn push(
        &mut self,
        n: Node<'_>,
        src: &str,
        kind: Kind,
        qname: String,
        name: String,
    ) -> String {
        self.push_with_visibility(n, src, kind, qname, name, true)
    }

    pub(crate) fn push_with_visibility(
        &mut self,
        n: Node<'_>,
        src: &str,
        kind: Kind,
        qname: String,
        name: String,
        is_public: bool,
    ) -> String {
        let pos = n.start_position();
        let id = node_id(self.repo_id, "ruby", &self.relpath, &qname, kind);
        let idx = self.out.symbols.len();
        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,
            doc: String::new(),
            hash: blake3::hash(text(n, src).as_bytes()).to_hex()[..16].to_string(),
        });
        self.id_to_idx.insert(id.clone(), idx);
        id
    }
}

// Re-export walk entry point — `crate::lang::ruby::walk` is called from
// framework/rails/handlers/routes.rs for route-block recursion.
pub(crate) use walk::walk;

// Re-exports for tests via `use super::*`.
#[cfg(test)]
pub(crate) use ckg_core::EdgeKind;

/// Strip Ruby string/symbol delimiters (`'foo'`, `"foo"`, `:foo`, heredocs).
/// Heredoc form: `<<~END\nbody\nEND` → return `body` (best-effort, single-line
/// body — multi-line heredocs in route values are vanishingly rare).
pub(crate) fn strip_quotes(s: &str) -> &str {
    let s = s.trim();
    if let Some(rest) = s
        .strip_prefix("<<~")
        .or_else(|| s.strip_prefix("<<-"))
        .or_else(|| s.strip_prefix("<<"))
    {
        // Skip the marker token until newline, then body until matching marker.
        if let Some(nl) = rest.find('\n') {
            let body = &rest[nl + 1..];
            let marker = rest[..nl].trim_matches(|c: char| c == '\'' || c == '"');
            if let Some(end) = body.rfind(marker) {
                return body[..end].trim();
            }
            return body;
        }
    }
    s.trim_start_matches(':')
        .trim_matches('\'')
        .trim_matches('"')
}

// Route-block helpers (handlers in framework/rails.rs):
//   `resources :name do … end` — `member do` → `/<name>/:id/<action>`;
//                                  `collection do` → `/<name>/<action>`
//   `namespace :admin do …` / `scope '/v1' do …` / `constraints … do …`

// Rails route declarations dispatched in framework/rails.rs:
//   `get '/foo' => 'foos#show'` / `post '/login', to: 'sessions#create'`
//   `root to: 'home#index'` / `mount Sidekiq::Web => '/sidekiq'`
//   `resources :posts` → 7 RESTful routes + member/collection blocks

pub fn extract(repo_id: &str, path: &Path, source: &str) -> Result<ParseOutput> {
    let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();
    let mut parser = crate::util::get_pooled_parser("ruby", &lang);
    let tree = crate::util::guarded_parse(&mut parser, source, "ruby");
    // 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,
        relpath: crate::util::path_to_relpath(path)?,
        module: module_name(path),
        out: ParseOutput::default(),
        route_prefix: String::new(),
        refinement_target: None,
        id_to_idx: std::collections::HashMap::new(),
    };
    walk(&mut ctx, tree.root_node(), source, "");
    crate::util::return_pooled_parser("ruby", parser);
    Ok(ctx.out)
}

#[cfg(test)]
mod tests;
