use std::collections::BTreeMap;

use anyhow::Result;

use crate::{types::extract_summary, GeneratedFile, GENERATED_BANNER};

use super::{
    model::{ModItem, ModItemKind, ModuleGraph, ModuleNode},
    paths::{module_index_path, module_index_rel_link, MODULE_DIR},
    RenderOutput, Renderer,
};

/// Renderer that emits one Markdown file per module with grouped sections.
///
/// Nested modules still receive their own files, but items are grouped
/// by kind within a single document instead of using per-item pages.
pub struct ModuleSingleRenderer;

impl Renderer for ModuleSingleRenderer {
    /// Render documentation using the single-page-per-module strategy.
    fn render(&self, graph: &ModuleGraph, out_root: &str) -> Result<RenderOutput> {
        let mut files: Vec<GeneratedFile> = Vec::new();
        render_module_single(&graph.crate_name, out_root, &graph.root, &mut files, true);

        let summary_fragment =
            module_tree_summary_modules_only(&graph.crate_name, out_root, &graph.root);

        Ok(RenderOutput {
            files,
            summary_fragment,
        })
    }
}

/// Recursively render a module with all items grouped in a single file.
///
/// Items are organized by kind (Functions, Structs, etc.) within the module page.
/// Child modules get their own files recursively.
fn render_module_single(
    crate_name: &str,
    out_root: &str,
    node: &ModuleNode,
    files: &mut Vec<GeneratedFile>,
    is_root: bool,
) {
    let rel_path = if is_root {
        format!("{}/index.md", out_root)
    } else {
        module_index_path(out_root, &node.fqpath)
    };
    let mut content = format!("{GENERATED_BANNER}\n\n");
    let title = if node.fqpath.is_empty() {
        format!("`{}` crate", crate_name)
    } else {
        format!("Module `{}`", node.fqpath)
    };
    content.push_str(&format!("# {}\n\n", title));

    if !node.docs.trim().is_empty() {
        content.push_str(node.docs.trim());
        content.push_str("\n\n");
    }

    if !node.children.is_empty() {
        content.push_str("## Modules\n\n");
        for m in &node.children {
            let link = if node.fqpath.is_empty() {
                format!("./{}/{}/index.md", MODULE_DIR, m.fqpath.replace("::", "/"))
            } else {
                module_index_rel_link(&node.fqpath, &m.fqpath)
            };
            content.push_str(&format!("- [{}]({})\n", m.name, link));
        }
        content.push('\n');
    }

    let mut groups: BTreeMap<&'static str, Vec<&ModItem>> = BTreeMap::new();
    for it in &node.items {
        let key = match it.kind {
            ModItemKind::Function => "Functions",
            ModItemKind::Struct => "Structs",
            ModItemKind::Enum => "Enums",
            ModItemKind::Trait => "Traits",
            ModItemKind::Constant => "Constants",
            ModItemKind::TypeAlias => "Type Aliases",
            ModItemKind::ReExport => "Re-exports",
        };
        groups.entry(key).or_default().push(it);
    }

    for (title, mut items) in groups {
        if items.is_empty() {
            continue;
        }
        items.sort_by(|a, b| a.name.cmp(&b.name));
        content.push_str(&format!("## {}\n\n", title));

        for item in &items {
            let anchor = make_anchor(&item.qualified);
            let summary = extract_summary(&item.docs);
            content.push_str(&format!("- [`{}`](#{}) — {}\n", item.name, anchor, summary));
        }
        content.push('\n');

        for item in items {
            let anchor = make_anchor(&item.qualified);
            content.push_str(&format!("<a id=\"{}\"></a>\n", anchor));
            content.push_str(&format!("### `{}`\n\n", item.name));
            if item.qualified != item.name {
                content.push_str(&format!("_Path_: `{}`\n\n", item.qualified));
            }
            if !item.docs.trim().is_empty() {
                content.push_str(item.docs.trim());
                content.push_str("\n\n");
            }
            if let Some(sig) = &item.signature {
                content.push_str("```rust\n");
                content.push_str(sig);
                content.push_str("\n```\n\n");
            }
        }
    }

    files.push(GeneratedFile {
        path: rel_path,
        content,
    });

    for m in &node.children {
        render_module_single(crate_name, out_root, m, files, false);
    }
}

/// Build SUMMARY.md fragment showing only modules (no per-item entries).
fn module_tree_summary_modules_only(crate_name: &str, out_root: &str, node: &ModuleNode) -> String {
    let mut s = String::new();
    s.push_str(&format!(
        "  - [`{}`](./{}/index.md)\n",
        crate_name, out_root
    ));
    for m in &node.children {
        push_modules_only(out_root, m, 2, &mut s);
    }
    s
}

/// Recursively append module-only entries to SUMMARY.md fragment.
fn push_modules_only(out_root: &str, node: &ModuleNode, indent: usize, buf: &mut String) {
    let indent_str = "  ".repeat(indent);
    let rel = format!("./{}", module_index_path(out_root, &node.fqpath));
    buf.push_str(&format!("{}- [`{}`]({})\n", indent_str, node.name, rel));
    for m in &node.children {
        push_modules_only(out_root, m, indent + 1, buf);
    }
}

/// Generate markdown anchor from item name.
///
/// Converts to lowercase and replaces `::` with `-`.
fn make_anchor(value: &str) -> String {
    let mut slug = String::new();
    let mut last_dash = false;
    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch.to_ascii_lowercase());
            last_dash = false;
        } else if ch == '_' {
            slug.push('_');
            last_dash = false;
        } else if !last_dash {
            slug.push('-');
            last_dash = true;
        }
    }
    while slug.ends_with('-') {
        slug.pop();
    }
    if slug.is_empty() {
        slug.push_str("item");
    }
    slug
}

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

    #[test]
    fn make_anchor_basic() {
        assert_eq!(make_anchor("foo"), "foo");
        assert_eq!(make_anchor("FooBar"), "foobar");
        assert_eq!(make_anchor("ABC"), "abc");
    }

    #[test]
    fn make_anchor_qualified_path() {
        assert_eq!(make_anchor("std::vec::Vec"), "std-vec-vec");
        assert_eq!(
            make_anchor("my_crate::utils::helper"),
            "my_crate-utils-helper"
        );
        assert_eq!(make_anchor("foo::bar::baz::qux"), "foo-bar-baz-qux");
    }

    #[test]
    fn make_anchor_preserves_underscores() {
        assert_eq!(make_anchor("my_function"), "my_function");
        assert_eq!(make_anchor("some_long_name"), "some_long_name");
        assert_eq!(make_anchor("_private"), "_private");
    }

    #[test]
    fn make_anchor_handles_generics() {
        assert_eq!(make_anchor("Vec<T>"), "vec-t");
        assert_eq!(make_anchor("HashMap<K, V>"), "hashmap-k-v");
        assert_eq!(make_anchor("Option<String>"), "option-string");
        assert_eq!(make_anchor("Result<T, E>"), "result-t-e");
    }

    #[test]
    fn make_anchor_empty_fallback() {
        assert_eq!(make_anchor(""), "item");
        assert_eq!(make_anchor(":::"), "item");
        assert_eq!(make_anchor("<>()"), "item");
        assert_eq!(make_anchor("   "), "item");
    }

    #[test]
    fn make_anchor_unicode() {
        // Unicode gets replaced with dashes, then trailing dashes are stripped
        assert_eq!(make_anchor("café"), "caf");
        assert_eq!(make_anchor("函数"), "item");
        // Mixed ASCII and Unicode - internal unicode becomes dash
        assert_eq!(make_anchor("foo_café_bar"), "foo_caf-_bar");
    }
}
