use std::path::Path;

use tree_sitter::Node as TsNode;

/// The text between the first `(` and the last `)` on a single signature line — a signature's
/// parameter list. Empty when there is no balanced pair (the caller then falls back to the name).
pub(crate) fn paren_contents(line: &str) -> &str {
    match (line.find('('), line.rfind(')')) {
        (Some(a), Some(b)) if b > a + 1 => &line[a + 1..b],
        _ => "",
    }
}

/// Normalize a derived signature summary to a single line capped near 120 chars. These summaries
/// feed BM25F identity scoring, not display, so they must stay short and free of newlines.
pub(crate) fn cap_summary(s: String) -> String {
    let one_line = s.split_whitespace().collect::<Vec<_>>().join(" ");
    if one_line.chars().count() <= 120 {
        one_line
    } else {
        let head: String = one_line.chars().take(117).collect();
        format!("{head}...")
    }
}

/// Prefer an authored module-level doc comment, otherwise derive a stable structural module
/// summary from language + relative module path. The fallback is intentionally factual: it gives
/// health and retrieval a non-empty identity surface without inventing domain semantics.
pub(crate) fn module_summary(
    authored: String,
    language: &str,
    title: &str,
    modpath: &str,
) -> String {
    if !authored.trim().is_empty() {
        return cap_summary(authored);
    }

    let module = if !modpath.trim().is_empty() {
        modpath
    } else {
        title
    };
    let language = language.trim();
    cap_summary(if language.is_empty() {
        format!("{module} — module")
    } else {
        format!("{module} — {language} module")
    })
}

/// `path:start-end` provenance string for an edge's evidence.
pub(crate) fn span_evidence(path: &str, node: TsNode) -> String {
    format!(
        "{}:{}-{}",
        path,
        node.start_position().row + 1,
        node.end_position().row + 1
    )
}

pub(crate) fn qualified_id(
    prefix: &str,
    modpath: &str,
    type_qual: Option<&str>,
    name: &str,
) -> String {
    match type_qual {
        Some(tq) => format!("{prefix}.{modpath}::{tq}::{name}"),
        None => format!("{prefix}.{modpath}::{name}"),
    }
}

pub(crate) fn node_name(node: TsNode, src: &str) -> Option<String> {
    let name = node.child_by_field_name("name")?;
    if matches!(
        name.kind(),
        "identifier" | "type_identifier" | "property_identifier" | "private_property_identifier"
    ) {
        Some(src[name.byte_range()].to_string())
    } else {
        None
    }
}

/// The `::`-joined module path of a file relative to the compile root, extension dropped. A
/// trailing `mod` component is dropped (`de/mod.rs` → `de`, not `de::mod`) — `mod.rs` IS its parent
/// directory's module, so the literal `mod` is an artifact, not a path segment.
pub(crate) fn mod_path(path: &Path, base: &Path) -> String {
    let rel = path.strip_prefix(base).unwrap_or(path).with_extension("");
    let mut comps: Vec<String> = rel
        .components()
        .map(|c| c.as_os_str().to_string_lossy().into_owned())
        .collect();
    if comps.last().is_some_and(|s| s == "mod") && comps.len() > 1 {
        comps.pop();
    }
    comps.join("::")
}

/// A file's module name: its stem, except `mod.rs` which names its parent directory.
pub(crate) fn module_title(path: &Path) -> String {
    let stem = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("module");
    if stem == "mod" {
        path.parent()
            .and_then(|p| p.file_name())
            .and_then(|s| s.to_str())
            .unwrap_or(stem)
            .to_string()
    } else {
        stem.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn module_summary_preserves_authored_text() {
        assert_eq!(
            module_summary(" Runtime graph cache. ".into(), "rust", "ryn", "ryn"),
            "Runtime graph cache."
        );
    }

    #[test]
    fn module_summary_falls_back_to_language_and_module_path() {
        assert_eq!(
            module_summary(String::new(), "rust", "health", "engine::health"),
            "engine::health — rust module"
        );
    }

    #[test]
    fn module_summary_handles_empty_language() {
        assert_eq!(
            module_summary(String::new(), "", "index", "pkg::index"),
            "pkg::index — module"
        );
    }
}
