use crate::config::SiteConfig;
use crate::graph::PageStore;
use crate::parser::ParsedPage;
use crate::render::toc::{self, TocEntry};
use minijinja::Value;

/// Resolve nav menu items: convert page names to URLs, use page icons when available.
/// When `nav.menu_tag` is set, auto-generates menu from pages that have that tag.
pub fn resolve_nav_menu(config: &SiteConfig, store: &PageStore) -> Vec<Value> {
    if let Some(ref tag) = config.nav.menu_tag {
        resolve_nav_menu_from_tag(tag, store)
    } else {
        resolve_nav_menu_from_config(config, store)
    }
}

/// Build menu from pages that have a specific tag (e.g. "menu").
/// Sorted by `menu-order::` property (ascending), then alphabetically by title.
fn resolve_nav_menu_from_tag(tag: &str, store: &PageStore) -> Vec<Value> {
    let tag_lower = tag.to_lowercase();
    let mut menu_pages: Vec<&crate::parser::ParsedPage> = store
        .pages
        .values()
        .filter(|page| page.meta.tags.iter().any(|t| t.to_lowercase() == tag_lower))
        .collect();

    menu_pages.sort_by(|a, b| {
        let ord_a = a.meta.menu_order.unwrap_or(i32::MAX);
        let ord_b = b.meta.menu_order.unwrap_or(i32::MAX);
        ord_a
            .cmp(&ord_b)
            .then_with(|| a.meta.title.cmp(&b.meta.title))
    });

    menu_pages
        .iter()
        .map(|page| {
            let url = format!("/{}", page.id);
            let icon = page.meta.icon.clone();
            // Title-case the label: capitalize first letter of each word
            let label = title_case(&page.meta.title);

            minijinja::context! {
                label => label,
                url => url,
                external => false,
                active => false,
                icon => icon,
            }
        })
        .collect()
}

/// Capitalize the first letter of each word.
fn title_case(s: &str) -> String {
    s.split_whitespace()
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
                None => String::new(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}

/// Generate a clean plain-text excerpt from raw markdown.
/// Strips wikilinks, headings, bullets, code fences, and collapses whitespace.
/// Truncates at word boundary to `max_chars` (default 160), appending `โ€ฆ`.
pub fn generate_excerpt(md: &str, max_chars: usize) -> String {
    let mut lines: Vec<&str> = Vec::new();
    let mut in_code_fence = false;

    for line in md.lines() {
        let trimmed = line.trim();

        // Skip code fences and their content
        if trimmed.starts_with("```") {
            in_code_fence = !in_code_fence;
            continue;
        }
        if in_code_fence {
            continue;
        }

        // Skip empty lines
        if trimmed.is_empty() {
            continue;
        }

        // Skip frontmatter markers
        if trimmed == "---" {
            continue;
        }

        // Skip heading markers but keep text
        let text = if trimmed.starts_with('#') {
            trimmed.trim_start_matches('#').trim()
        } else {
            trimmed
        };

        // Strip bullet prefixes
        let text = text
            .strip_prefix("- ")
            .or_else(|| text.strip_prefix("* "))
            .unwrap_or(text);

        if text.is_empty() {
            continue;
        }

        lines.push(text);
    }

    let joined = lines.join(" ");

    // Strip [[wikilink]] syntax โ†’ keep inner text
    let mut result = String::with_capacity(joined.len());
    let chars: Vec<char> = joined.chars().collect();
    let len = chars.len();
    let mut i = 0;
    while i < len {
        if i + 1 < len && chars[i] == '[' && chars[i + 1] == '[' {
            // Find closing ]]
            i += 2;
            while i + 1 < len && !(chars[i] == ']' && chars[i + 1] == ']') {
                result.push(chars[i]);
                i += 1;
            }
            if i + 1 < len {
                i += 2; // skip ]]
            }
        } else {
            result.push(chars[i]);
            i += 1;
        }
    }

    // Strip <div class="query-fallback"><code>...</code><div class="query-note">This query uses advanced features. View in Logseq for live results.</div></div> and {{embed ...}} expressions
    let mut clean = String::with_capacity(result.len());
    let chars: Vec<char> = result.chars().collect();
    let len = chars.len();
    let mut i = 0;
    while i < len {
        if i + 1 < len && chars[i] == '{' && chars[i + 1] == '{' {
            // Skip until }}
            i += 2;
            while i + 1 < len && !(chars[i] == '}' && chars[i + 1] == '}') {
                i += 1;
            }
            if i + 1 < len {
                i += 2;
            }
        } else {
            clean.push(chars[i]);
            i += 1;
        }
    }

    // Collapse whitespace
    let collapsed: String = clean.split_whitespace().collect::<Vec<_>>().join(" ");

    // Truncate at word boundary (char-aware for UTF-8)
    let char_count = collapsed.chars().count();
    if char_count <= max_chars {
        return collapsed;
    }

    let truncated: String = collapsed.chars().take(max_chars).collect();
    if let Some(last_space) = truncated.rfind(' ') {
        format!("{}โ€ฆ", &truncated[..last_space])
    } else {
        format!("{}โ€ฆ", truncated)
    }
}

/// Build menu from static config entries (original behavior).
fn resolve_nav_menu_from_config(config: &SiteConfig, store: &PageStore) -> Vec<Value> {
    config
        .nav
        .menu
        .iter()
        .map(|item| {
            let slug = item
                .page
                .as_ref()
                .map(|p| crate::parser::slugify_page_name(p));
            let url = if let Some(ref s) = slug {
                format!("/{}", s)
            } else if let Some(ref url) = item.url {
                url.clone()
            } else {
                "#".to_string()
            };

            // Prefer page's own icon:: property over nav config icon
            let icon = slug
                .as_ref()
                .and_then(|s| store.pages.get(s))
                .and_then(|p| p.meta.icon.clone())
                .or_else(|| item.icon.clone());

            minijinja::context! {
                label => item.label.clone(),
                url => url,
                external => item.external,
                active => false,
                icon => icon,
            }
        })
        .collect()
}

/// Build the complete template context for rendering a page.
pub fn build_page_context(
    page: &ParsedPage,
    html_body: &str,
    toc_entries: &[TocEntry],
    store: &PageStore,
    config: &SiteConfig,
) -> Value {
    let backlinks = store.get_backlinks(&page.id);
    let backlink_data: Vec<Value> = backlinks
        .iter()
        .map(|bl| {
            minijinja::context! {
                title => bl.title.clone(),
                url => bl.url.clone(),
            }
        })
        .collect();

    let word_count = page.content_md.split_whitespace().count();
    let reading_time = (word_count as f64 / 200.0).ceil() as usize;

    let children: Vec<Value> = if page.namespace.is_some() {
        vec![] // This page is a child, not a parent
    } else {
        // Check if this page is a namespace parent
        let page_name_lower = page.meta.title.to_lowercase();
        store
            .get_namespace_children(&page_name_lower)
            .iter()
            .map(|child| {
                minijinja::context! {
                    title => child.meta.title.rsplit('/').next().unwrap_or(&child.meta.title).to_string(),
                    url => format!("/{}", child.id),
                }
            })
            .collect()
    };

    let nav_menu = resolve_nav_menu(config, store);

    // Generate TOC HTML if page has headings
    let toc_html = if toc_entries.len() >= 2 {
        toc::render_toc_html(toc_entries)
    } else {
        String::new()
    };

    // Build namespace breadcrumb parts
    let namespace_parts: Vec<Value> = if let Some(ref ns) = page.namespace {
        let segments: Vec<&str> = ns.split('/').collect();
        let mut parts = Vec::new();
        for (i, seg) in segments.iter().enumerate() {
            let full_path = segments[..=i].join("/");
            let slug = crate::parser::slugify_page_name(&full_path);
            parts.push(minijinja::context! {
                name => seg.to_string(),
                url => format!("/{}", slug),
            });
        }
        parts
    } else {
        vec![]
    };

    // Resolve description: frontmatter description > auto-excerpt > title fallback
    let description = page
        .meta
        .properties
        .get("description")
        .filter(|d| !d.is_empty())
        .cloned()
        .unwrap_or_else(|| {
            let excerpt = generate_excerpt(&page.content_md, 160);
            if excerpt.is_empty() {
                page.meta.title.clone()
            } else {
                excerpt
            }
        });

    let canonical_url = format!("{}/{}", config.site.base_url, page.id);

    // Resolve favicon: page icon > namespace parent icon > site favicon
    let favicon = page
        .meta
        .icon
        .clone()
        .or_else(|| {
            // Walk up namespace parents to find an icon
            if let Some(ref ns) = page.namespace {
                let segments: Vec<&str> = ns.split('/').collect();
                for i in (0..segments.len()).rev() {
                    let parent_path = segments[..=i].join("/");
                    let parent_slug = crate::parser::slugify_page_name(&parent_path);
                    if let Some(parent) = store.pages.get(&parent_slug) {
                        if parent.meta.icon.is_some() {
                            return parent.meta.icon.clone();
                        }
                    }
                }
            }
            None
        })
        .or_else(|| config.site.favicon.clone());

    minijinja::context! {
        site => config.site,
        style => config.style,
        nav_menu => nav_menu,
        graph => config.graph,
        analytics => config.analytics,
        search => config.search,
        favicon => favicon,
        description => description,
        canonical_url => canonical_url,
        page => minijinja::context! {
            title => page.meta.title.clone(),
            display_name => {
                let base = page.meta.title.rsplit('/').next().unwrap_or(&page.meta.title);
                match page.kind {
                    crate::parser::PageKind::Page | crate::parser::PageKind::Journal => format!("{}.md", base),
                    crate::parser::PageKind::File => base.to_string(),
                }
            },
            id => page.id.clone(),
            html_content => html_body,
            meta => page.meta.properties.clone(),
            tags => page.meta.tags.clone(),
            aliases => page.meta.aliases.clone(),
            url => format!("/{}", page.id),
            namespace => page.namespace.clone(),
            namespace_parts => namespace_parts,
            children => children,
            word_count => word_count,
            reading_time_minutes => reading_time,
            date => page.meta.date.map(|d| d.format("%Y-%m-%d").to_string()),
            icon => page.meta.icon.clone(),
            kind => format!("{:?}", page.kind),
            toc => toc_html,
            focus => store.focus.get(&page.id).copied().unwrap_or(0.0),
        },
        backlinks => backlink_data,
    }
}

Local Graph