//! Category and tag filtering functionality
//!
//! Filters categories and tags to only show those with actual content

use std::collections::{HashMap, HashSet};
use std::path::Path;

use crate::categories::cache::load_meta_config;

/// Get all available categories with their emojis (filtered by actual content
/// availability)
pub fn get_all_categories(content_type: &str, language: &str) -> HashMap<String, String> {
    match load_meta_config(content_type, language) {
        Ok(config) => {
            // Filter categories to only include those with actual posts
            filter_categories_with_content(config.categories_emojis, content_type, language)
        }
        Err(e) => {
            tracing::warn!("Failed to load meta config: {}", e);
            HashMap::new()
        }
    }
}

/// Get all categories for a content type and language, without filtering
/// (includes all defined categories) This is used for routing validation where
/// we want to allow navigation to any defined category
pub fn get_all_categories_unfiltered(
    content_type: &str,
    language: &str,
) -> HashMap<String, String> {
    match load_meta_config(content_type, language) {
        Ok(config) => config.categories_emojis,
        Err(e) => {
            tracing::warn!("Failed to load meta config: {}", e);

            // Return empty HashMap for error cases
            // Both WASM and server builds should handle gracefully
            HashMap::new()
        }
    }
}

/// Filter categories to only include those that have actual content posts
fn filter_categories_with_content(
    all_categories: HashMap<String, String>,
    content_type: &str,
    language: &str,
) -> HashMap<String, String> {
    #[cfg(not(target_arch = "wasm32"))]
    {
        let content_root =
            std::env::var("SITE_CONTENT_PATH").unwrap_or_else(|_| "site/content".to_string());
        let content_dir = Path::new(&content_root).join(content_type).join(language);

        if !content_dir.exists() {
            tracing::warn!("Content directory does not exist: {:?}", content_dir);
            return HashMap::new();
        }

        // Get list of categories that actually have content
        let available_categories = get_categories_with_posts(&content_dir);

        let total_categories = all_categories.len();
        let mut filtered_categories = HashMap::new();

        // Filter the configured categories to only include those with content
        for (category, emoji) in all_categories {
            if available_categories.contains(&category) {
                filtered_categories.insert(category, emoji);
            }
        }

        tracing::debug!(
            "Filtered categories for {}/{}: {} total, {} with content",
            content_type,
            language,
            total_categories,
            filtered_categories.len()
        );

        filtered_categories
    }

    #[cfg(target_arch = "wasm32")]
    {
        // For WASM, we need to filter based on the available content data
        // Since we can't access the filesystem, we'll filter based on the index.json
        // data that should be available via the content resolver
        use crate::content_resolver::get_content_index;

        let available_categories = match get_content_index(content_type, language) {
            Ok(index_data) => {
                let mut categories_with_content = std::collections::HashSet::new();

                if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&index_data) {
                    if let Some(items) = json_value.get(content_type).and_then(|v| v.as_array()) {
                        for item in items {
                            // Extract category from single category field
                            if let Some(category) = item.get("category").and_then(|c| c.as_str()) {
                                categories_with_content.insert(category.to_string());
                            }
                            // Extract from categories array if present
                            if let Some(categories_array) =
                                item.get("categories").and_then(|c| c.as_array())
                            {
                                for cat in categories_array {
                                    if let Some(cat_str) = cat.as_str() {
                                        categories_with_content.insert(cat_str.to_string());
                                    }
                                }
                            }
                        }
                    }
                }

                tracing::debug!(
                    "WASM: Found {} categories with content: {:?}",
                    categories_with_content.len(),
                    categories_with_content
                );

                categories_with_content
            }
            Err(e) => {
                tracing::warn!(
                    "WASM: Failed to load content index: {}, falling back to unfiltered categories",
                    e
                );
                // Fallback: return all configured categories instead of empty set
                // This ensures filters work during SPA navigation when content index fails to
                // load Better UX: show all categories rather than hiding the
                // filter entirely
                all_categories.keys().cloned().collect()
            }
        };

        let total_categories = all_categories.len();
        let mut filtered_categories = HashMap::new();

        // Filter the configured categories to only include those with content
        for (category, emoji) in all_categories {
            if available_categories.contains(&category) {
                filtered_categories.insert(category, emoji);
            }
        }

        tracing::debug!(
            "WASM: Filtered categories for {}/{}: {} total, {} with content",
            content_type,
            language,
            total_categories,
            filtered_categories.len()
        );

        filtered_categories
    }
}

/// Scan content directory and return list of categories that have at least one
/// post Reads from index.json files to get actual content items with their
/// categories
fn get_categories_with_posts(content_dir: &Path) -> HashSet<String> {
    use std::fs;

    // Extract content type from the directory path
    // Path structure: content/{content_type}/{language}
    let content_type = extract_content_type_from_path(content_dir);

    // Look for index.json file in the content directory
    let index_file = content_dir.join("index.json");

    // Early return if file doesn't exist
    let content = match fs::read_to_string(&index_file) {
        Ok(content) => content,
        Err(_) => {
            tracing::warn!("No index.json found at {}", index_file.display());
            return HashSet::new();
        }
    };

    tracing::debug!(
        "Reading categories from index.json for content_type '{}': {}",
        content_type,
        index_file.display()
    );

    // Parse JSON, early return on error
    let json_value: serde_json::Value = match serde_json::from_str(&content) {
        Ok(value) => value,
        Err(e) => {
            tracing::warn!("Failed to parse JSON from {}: {}", index_file.display(), e);
            return HashSet::new();
        }
    };

    // Extract categories from the parsed JSON
    extract_categories_from_json(&json_value, &content_type, &index_file)
}

/// Extract content type from directory path
fn extract_content_type_from_path(content_dir: &Path) -> String {
    content_dir
        .parent() // Gets parent of language dir (e.g., "blog" from "blog/en")
        .and_then(|p| p.file_name())
        .and_then(|n| n.to_str())
        .map(|s| s.to_string())
        .unwrap_or_else(|| "content".to_string())
}

/// Extract categories from parsed JSON data
fn extract_categories_from_json(
    json_value: &serde_json::Value,
    content_type: &str,
    index_file: &Path,
) -> HashSet<String> {
    let mut categories_with_posts = HashSet::new();

    // Get the JSON object
    let obj = match json_value.as_object() {
        Some(obj) => obj,
        None => {
            tracing::warn!("JSON is not an object in {}", index_file.display());
            return categories_with_posts;
        }
    };

    // Get the content array for this content type
    let items = match obj.get(content_type).and_then(|v| v.as_array()) {
        Some(items) => items,
        None => {
            tracing::warn!(
                "No content array found for key '{}' in {}",
                content_type,
                index_file.display()
            );
            return categories_with_posts;
        }
    };

    tracing::debug!(
        "Found {} items under content_type key '{}'",
        items.len(),
        content_type
    );

    // Process each item
    for item in items {
        collect_categories_from_item(item, &mut categories_with_posts);
    }

    tracing::debug!(
        "Found {} unique categories with posts",
        categories_with_posts.len()
    );

    categories_with_posts
}

/// Collect categories from a single content item
fn collect_categories_from_item(item: &serde_json::Value, categories: &mut HashSet<String>) {
    // Extract single category field
    if let Some(category) = item.get("category").and_then(|c| c.as_str()) {
        categories.insert(category.to_string());
        tracing::debug!("Found category '{}' from item", category);
    }

    // Extract from categories array if present
    if let Some(categories_array) = item.get("categories").and_then(|c| c.as_array()) {
        for cat in categories_array {
            if let Some(cat_str) = cat.as_str() {
                categories.insert(cat_str.to_string());
                tracing::debug!("Found category '{}' from categories array", cat_str);
            }
        }
    }
}

/// Extract tags from parsed JSON data
fn extract_tags_from_json(
    json_value: &serde_json::Value,
    content_type: &str,
    index_file: &Path,
) -> HashSet<String> {
    let mut tags_with_posts = HashSet::new();

    // Get the JSON object
    let obj = match json_value.as_object() {
        Some(obj) => obj,
        None => {
            tracing::warn!("JSON is not an object in {}", index_file.display());
            return tags_with_posts;
        }
    };

    // Get the content array for this content type
    let items = match obj.get(content_type).and_then(|v| v.as_array()) {
        Some(items) => items,
        None => {
            tracing::warn!(
                "No content array found for key '{}' in {}",
                content_type,
                index_file.display()
            );
            return tags_with_posts;
        }
    };

    tracing::debug!(
        "Found {} items under content_type key '{}'",
        items.len(),
        content_type
    );

    // Process each item
    for item in items {
        collect_tags_from_item(item, &mut tags_with_posts);
    }

    tracing::debug!("Found {} unique tags with posts", tags_with_posts.len());

    tags_with_posts
}

/// Collect tags from a single content item
fn collect_tags_from_item(item: &serde_json::Value, tags: &mut HashSet<String>) {
    // Extract single tag field
    if let Some(tag) = item.get("tag").and_then(|t| t.as_str()) {
        tags.insert(tag.to_string());
        tracing::debug!("Found tag '{}' from item", tag);
    }

    // Extract from tags array if present
    if let Some(tags_array) = item.get("tags").and_then(|t| t.as_array()) {
        for tag in tags_array {
            if let Some(tag_str) = tag.as_str() {
                tags.insert(tag_str.to_string());
                tracing::debug!("Found tag '{}' from tags array", tag_str);
            }
        }
    }
}

/// Extract categories from markdown frontmatter
fn extract_categories_from_frontmatter(content: &str) -> Option<Vec<String>> {
    extract_frontmatter_array(content, "categories")
}

/// Get all available tags with their emojis (filtered by actual content
/// availability)
pub fn get_all_tags(content_type: &str, language: &str) -> HashMap<String, String> {
    match load_meta_config(content_type, language) {
        Ok(config) => {
            // Filter tags to only include those with actual posts
            filter_tags_with_content(config.tags_emojis, content_type, language)
        }
        Err(e) => {
            tracing::warn!("Failed to load meta config: {}", e);
            HashMap::new()
        }
    }
}

/// Get all tags for a content type and language, without filtering (includes
/// all defined tags) This is used for routing validation where we want to allow
/// navigation to any defined tag
pub fn get_all_tags_unfiltered(content_type: &str, language: &str) -> HashMap<String, String> {
    match load_meta_config(content_type, language) {
        Ok(config) => config.tags_emojis,
        Err(e) => {
            tracing::warn!("Failed to load meta config: {}", e);
            HashMap::new()
        }
    }
}

/// Filter tags to only include those that have actual content posts
fn filter_tags_with_content(
    all_tags: HashMap<String, String>,
    content_type: &str,
    language: &str,
) -> HashMap<String, String> {
    #[cfg(not(target_arch = "wasm32"))]
    {
        use std::fs;

        let content_root =
            std::env::var("SITE_CONTENT_PATH").unwrap_or_else(|_| "site/content".to_string());
        let content_dir = Path::new(&content_root).join(content_type).join(language);

        if !content_dir.exists() {
            tracing::warn!("Content directory does not exist: {:?}", content_dir);
            return HashMap::new();
        }

        // Get list of tags that actually have content
        let available_tags = get_tags_with_posts(&content_dir);

        let total_tags = all_tags.len();
        let mut filtered_tags = HashMap::new();

        // Filter the configured tags to only include those with content
        for (tag, emoji) in all_tags {
            if available_tags.contains(&tag) {
                filtered_tags.insert(tag, emoji);
            }
        }

        tracing::debug!(
            "Filtered tags for {}/{}: {} total, {} with content",
            content_type,
            language,
            total_tags,
            filtered_tags.len()
        );

        filtered_tags
    }

    #[cfg(target_arch = "wasm32")]
    {
        // For WASM, we need to filter based on the available content data
        // Since we can't access the filesystem, we'll filter based on the index.json
        // data that should be available via the content resolver
        use crate::content_resolver::get_content_index;

        let available_tags = match get_content_index(content_type, language) {
            Ok(index_data) => {
                let mut tags_with_content = std::collections::HashSet::new();

                if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&index_data) {
                    if let Some(items) = json_value.get(content_type).and_then(|v| v.as_array()) {
                        for item in items {
                            // Extract tag from single tag field
                            if let Some(tag) = item.get("tag").and_then(|t| t.as_str()) {
                                tags_with_content.insert(tag.to_string());
                            }
                            // Extract from tags array if present
                            if let Some(tags_array) = item.get("tags").and_then(|t| t.as_array()) {
                                for tag in tags_array {
                                    if let Some(tag_str) = tag.as_str() {
                                        tags_with_content.insert(tag_str.to_string());
                                    }
                                }
                            }
                        }
                    }
                }

                tracing::debug!(
                    "WASM: Found {} tags with content: {:?}",
                    tags_with_content.len(),
                    tags_with_content
                );

                tags_with_content
            }
            Err(e) => {
                tracing::warn!(
                    "WASM: Failed to load content index: {}, falling back to unfiltered tags",
                    e
                );
                // Fallback: return all configured tags instead of empty set
                // This ensures filters work during SPA navigation when content index fails to
                // load Better UX: show all tags rather than hiding the filter
                // entirely
                all_tags.keys().cloned().collect()
            }
        };

        let total_tags = all_tags.len();
        let mut filtered_tags = HashMap::new();

        // Filter the configured tags to only include those with content
        for (tag, emoji) in all_tags {
            if available_tags.contains(&tag) {
                filtered_tags.insert(tag, emoji);
            }
        }

        tracing::debug!(
            "WASM: Filtered tags for {}/{}: {} total, {} with content",
            content_type,
            language,
            total_tags,
            filtered_tags.len()
        );

        filtered_tags
    }
}

/// Scan content directory and return list of tags that have at least one post
/// Reads from index.json files to get actual content items with their tags
fn get_tags_with_posts(content_dir: &Path) -> HashSet<String> {
    use std::fs;

    // Extract content type from the directory path
    // Path structure: content/{content_type}/{language}
    let content_type = extract_content_type_from_path(content_dir);

    // Look for index.json file in the content directory
    let index_file = content_dir.join("index.json");

    // Early return if file doesn't exist
    let content = match fs::read_to_string(&index_file) {
        Ok(content) => content,
        Err(_) => {
            tracing::warn!("No index.json found at {}", index_file.display());
            return HashSet::new();
        }
    };

    tracing::debug!(
        "Reading tags from index.json for content_type '{}': {}",
        content_type,
        index_file.display()
    );

    // Parse JSON, early return on error
    let json_value: serde_json::Value = match serde_json::from_str(&content) {
        Ok(value) => value,
        Err(e) => {
            tracing::warn!("Failed to parse JSON from {}: {}", index_file.display(), e);
            return HashSet::new();
        }
    };

    // Extract tags from the parsed JSON
    extract_tags_from_json(&json_value, &content_type, &index_file)
}

/// Process a directory entry to extract tags from markdown files
fn process_entry_for_tags(entry: &std::fs::DirEntry, tags: &mut HashSet<String>) {
    // Check if it's a file
    let file_type = match entry.file_type() {
        Ok(ft) => ft,
        Err(_) => return,
    };

    if !file_type.is_file() {
        return;
    }

    // Check if it's a markdown file (excluding index.md)
    let file_name = entry.file_name();
    let file_name_str = match file_name.to_str() {
        Some(name) => name,
        None => return,
    };

    if !file_name_str.ends_with(".md") || file_name_str == "index.md" {
        return;
    }

    // Read and process the file
    if let Ok(content) = std::fs::read_to_string(entry.path()) {
        if let Some(file_tags) = extract_tags_from_frontmatter(&content) {
            tags.extend(file_tags);
        }
    }
}

/// Extract tags from markdown frontmatter
fn extract_tags_from_frontmatter(content: &str) -> Option<Vec<String>> {
    extract_frontmatter_array(content, "tags")
}

/// Generic function to extract a string array field from TOML frontmatter
fn extract_frontmatter_array(content: &str, field_name: &str) -> Option<Vec<String>> {
    // Check for TOML frontmatter markers
    if !content.starts_with("+++") {
        return None;
    }

    // Find the end of frontmatter
    let frontmatter_end = content[3..].find("+++")?;
    let frontmatter = &content[3..3 + frontmatter_end];

    // Parse TOML
    let parsed: toml::Value = toml::from_str(frontmatter).ok()?;

    // Extract the array field
    let array = parsed.get(field_name)?.as_array()?;

    // Convert to Vec<String>
    let result: Vec<String> = array
        .iter()
        .filter_map(|v| v.as_str())
        .map(|s| s.to_string())
        .collect();

    if result.is_empty() {
        None
    } else {
        Some(result)
    }
}
