use std::fs::File;
use std::path::Path;

use anyhow::{Context, Result};
use rustdoc_types::{Crate, Item, ItemEnum, Module, Visibility};
use serde_json::from_reader;

use crate::types::{render_function_signature, render_type, visibility_prefix};

use super::model::{ModItem, ModItemKind, ModuleGraph, ModuleNode};

/// Parse rustdoc JSON and build an in-memory module graph.
///
/// Returns `None` if the root item is not a module. Private items are filtered
/// unless `include_private` is `true`.
pub fn build_module_graph(path: &Path, include_private: bool) -> Result<Option<ModuleGraph>> {
    let file = File::open(path)
        .with_context(|| format!("failed to open rustdoc JSON `{}`", path.display()))?;
    let krate: Crate =
        from_reader(file).with_context(|| format!("failed to parse `{}`", path.display()))?;

    let fallback_name = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown")
        .to_string();

    let crate_name = krate
        .paths
        .get(&krate.root)
        .and_then(|summary| summary.path.last())
        .cloned()
        .unwrap_or(fallback_name);

    let root_item = match krate.index.get(&krate.root) {
        Some(item) => item,
        None => return Ok(None),
    };

    if let ItemEnum::Module(module) = &root_item.inner {
        let root = build_module_node(
            &krate,
            module,
            &crate_name,
            "",
            root_item.docs.as_deref(),
            include_private,
        );
        Ok(Some(ModuleGraph { crate_name, root }))
    } else {
        Ok(None)
    }
}

/// Recursively build a module node from rustdoc JSON.
///
/// Traverses child items, filtering by visibility, and recursively processes submodules.
fn build_module_node(
    krate: &Crate,
    module: &Module,
    crate_name: &str,
    mod_path: &str,
    docs: Option<&str>,
    include_private: bool,
) -> ModuleNode {
    let mut node = ModuleNode {
        name: if mod_path.is_empty() {
            crate_name.to_string()
        } else {
            mod_path.split("::").last().unwrap_or(mod_path).to_string()
        },
        fqpath: mod_path.to_string(),
        docs: docs.unwrap_or("").to_string(),
        children: Vec::new(),
        items: Vec::new(),
    };

    for child_id in &module.items {
        let child = match krate.index.get(child_id) {
            Some(i) => i,
            None => continue,
        };

        if !include_private && !matches!(child.visibility, Visibility::Public) {
            continue;
        }

        match &child.inner {
            ItemEnum::Module(sub) => {
                let next_path = if mod_path.is_empty() {
                    child.name.clone().unwrap_or_default()
                } else {
                    format!("{}::{}", mod_path, child.name.as_deref().unwrap_or(""))
                };
                let subnode = build_module_node(
                    krate,
                    sub,
                    crate_name,
                    &next_path,
                    child.docs.as_deref(),
                    include_private,
                );
                node.children.push(subnode);
            }
            ItemEnum::Function(func) => {
                let name = child.name.clone().unwrap_or_default();
                let qualified = if mod_path.is_empty() {
                    name.clone()
                } else {
                    format!("{}::{}", mod_path, name)
                };
                let signature = Some(render_function_signature(&name, func, &child.visibility));
                node.items.push(ModItem {
                    kind: ModItemKind::Function,
                    name,
                    qualified,
                    docs: child.docs.clone().unwrap_or_default(),
                    signature,
                });
            }
            ItemEnum::Struct(_) => {
                push_struct_like(&mut node, ModItemKind::Struct, child, mod_path, "struct")
            }
            ItemEnum::Enum(_) => {
                push_struct_like(&mut node, ModItemKind::Enum, child, mod_path, "enum")
            }
            ItemEnum::Trait(_) => {
                push_struct_like(&mut node, ModItemKind::Trait, child, mod_path, "trait")
            }
            ItemEnum::TypeAlias(alias) => {
                let name = child.name.clone().unwrap_or_default();
                let qualified = if mod_path.is_empty() {
                    name.clone()
                } else {
                    format!("{}::{}", mod_path, name)
                };
                let signature = format_type_alias_signature(&child.visibility, &name, alias);
                node.items.push(ModItem {
                    kind: ModItemKind::TypeAlias,
                    name,
                    qualified,
                    docs: child.docs.clone().unwrap_or_default(),
                    signature: Some(signature),
                });
            }
            ItemEnum::Constant { type_, .. } => {
                let name = child.name.clone().unwrap_or_default();
                let qualified = if mod_path.is_empty() {
                    name.clone()
                } else {
                    format!("{}::{}", mod_path, name)
                };
                let signature = Some(format_constant_signature(&child.visibility, &name, type_));
                node.items.push(ModItem {
                    kind: ModItemKind::Constant,
                    name,
                    qualified,
                    docs: child.docs.clone().unwrap_or_default(),
                    signature,
                });
            }
            ItemEnum::Use(u) => {
                let raw = child
                    .name
                    .clone()
                    .or_else(|| Some(u.source.split("::").last().unwrap_or("").to_string()))
                    .unwrap_or_else(|| "reexport".to_string());
                let name = if raw.is_empty() {
                    "reexport".to_string()
                } else {
                    raw
                };
                let qualified = if mod_path.is_empty() {
                    name.clone()
                } else {
                    format!("{}::{}", mod_path, name)
                };
                node.items.push(ModItem {
                    kind: ModItemKind::ReExport,
                    name,
                    qualified,
                    docs: child.docs.clone().unwrap_or_default(),
                    signature: Some(format_use_signature(&child.visibility, &u.source)),
                });
            }
            _ => {}
        }
    }

    node.children.sort_by(|a, b| a.name.cmp(&b.name));
    node.items.sort_by(|a, b| a.name.cmp(&b.name));
    node
}

/// Add a struct-like item (struct, enum, trait) to the module node.
fn push_struct_like(
    node: &mut ModuleNode,
    kind: ModItemKind,
    item: &Item,
    mod_path: &str,
    keyword: &str,
) {
    let name = item.name.clone().unwrap_or_default();
    let qualified = if mod_path.is_empty() {
        name.clone()
    } else {
        format!("{}::{}", mod_path, name)
    };
    let signature = Some(format_item_declaration(keyword, &name, &item.visibility));
    node.items.push(ModItem {
        kind,
        name,
        qualified,
        docs: item.docs.clone().unwrap_or_default(),
        signature,
    });
}

/// Format a basic item declaration with visibility.
///
/// Returns strings like `"pub struct Foo"` or `"trait Bar"`.
fn format_item_declaration(keyword: &str, name: &str, visibility: &Visibility) -> String {
    match visibility_prefix(visibility) {
        Some(vis) => format!("{} {} {}", vis, keyword, name),
        None => format!("{} {}", keyword, name),
    }
}

/// Format a type alias signature with its definition.
///
/// Returns strings like `"pub type Result<T> = std::result::Result<T, Error>;"`.
fn format_type_alias_signature(
    visibility: &Visibility,
    name: &str,
    alias: &rustdoc_types::TypeAlias,
) -> String {
    let decl = format_item_declaration("type", name, visibility);
    format!("{} = {};", decl, render_type(&alias.type_))
}

/// Format a constant signature with its type.
///
/// Returns strings like `"pub const MAX: usize;"`.
fn format_constant_signature(
    visibility: &Visibility,
    name: &str,
    ty: &rustdoc_types::Type,
) -> String {
    let decl = format_item_declaration("const", name, visibility);
    format!("{}: {};", decl, render_type(ty))
}

/// Format a re-export signature.
///
/// Returns strings like `"pub use std::io::Error;"`.
fn format_use_signature(visibility: &Visibility, source: &str) -> String {
    let decl = match visibility_prefix(visibility) {
        Some(vis) => format!("{} use {}", vis, source),
        None => format!("use {}", source),
    };
    format!("{};", decl)
}
