//! Unified content loading utilities
//! Single source of truth for all content loading operations

use std::sync::Arc;

use once_cell::sync::Lazy;

use super::models::ContentIndex;
use super::resolvers::resolve_content_slug_with_language_priority;
use crate::cache::content::ContentCache;
use crate::cache::{current_timestamp, ContentCacheKey};

/// Global content cache instance
/// Shared across all content loading operations for consistency
static GLOBAL_CONTENT_CACHE: Lazy<Arc<ContentCache>> = Lazy::new(|| Arc::new(ContentCache::new()));

/// Unified content index loader with integrated caching
/// Single source of truth for all content loading operations
/// Uses content-kinds.toml configuration to determine content structure
pub fn load_content_index(
    content_type: &str,
    language: &str,
) -> Result<ContentIndex, Box<dyn std::error::Error>> {
    // Try to get from cache first
    let cache_key = ContentCacheKey::new(format!("{}_index", content_type), language)
        .with_language(language.to_string());

    if let Some(cached_value) = GLOBAL_CONTENT_CACHE.get_content(&cache_key) {
        // Deserialize from JSON value back to ContentIndex
        if let Ok(cached_index) = serde_json::from_value::<ContentIndex>((*cached_value).clone()) {
            tracing::debug!(
                "📦 Cache HIT: {} index for '{}' loaded from cache",
                content_type,
                language
            );
            return Ok(cached_index);
        }
    }

    tracing::debug!(
        "📦 Cache MISS: Loading {} index for '{}' from source",
        content_type,
        language
    );
    let config = crate::config::ContentManageConfig::from_env();

    // Try to load from embedded resources first (WASM compatible)
    let content = load_embedded_content(&config, content_type, language, "index.json").or_else(
        |embedded_err| {
            tracing::debug!(
                "Embedded content loading failed for {} {}: {}",
                content_type,
                language,
                embedded_err
            );

            #[cfg(target_arch = "wasm32")]
            {
                // Try synchronous DOM read (data embedded by server shell.rs) before async HTTP
                try_load_content_from_dom(content_type, language)
                    .map_err(|e| -> Box<dyn std::error::Error> { e })
            }

            #[cfg(not(target_arch = "wasm32"))]
            {
                // For SSR, also use HTTP fetch for consistency with client
                tracing::debug!(
                    "SSR: Attempting HTTP fetch for {}/{}",
                    content_type,
                    language
                );
                fetch_content_index_http_ssr(content_type, language)
            }
        },
    );

    // Second fallback: HTTP request for WASM client
    #[cfg(all(target_arch = "wasm32", not(feature = "ssr")))]
    let content = content.or_else(|file_err| {
        tracing::debug!("All local loading methods failed: {}", file_err);
        tracing::debug!(
            "Attempting HTTP request fallback for {}/{}",
            content_type,
            language
        );
        fetch_content_index_http(content_type, language)
    });

    let content = content?;

    tracing::debug!(
        "{} content loaded for {}: {} characters",
        content_type,
        language,
        content.len()
    );

    let index: serde_json::Value = serde_json::from_str(&content)?;
    let mut content_index = ContentIndex {
        language: language.to_string(),
        content_type: content_type.to_string(),
        ..Default::default()
    };

    // Try to parse items from various possible keys in the JSON
    // Start with generic keys and add all known content types dynamically
    let mut possible_keys = vec![
        content_type, // Use content type as key
        "items",      // Generic items key
        "content",    // Generic content key
        "posts",      // Generic posts key
    ];

    // Add all available content types as possible keys dynamically
    let available_content_types = crate::get_all_content_types();
    let mut dynamic_keys = Vec::new();
    for ct in &available_content_types {
        // Add the content type itself and common variations
        possible_keys.push(ct);
        dynamic_keys.push(format!("{}_posts", ct));
        dynamic_keys.push(format!("{}s", ct)); // plural form
    }
    // Add dynamic keys to possible_keys
    for key in &dynamic_keys {
        possible_keys.push(key);
    }

    // Remove duplicates while preserving order
    possible_keys.dedup();

    for key in &possible_keys {
        if let Some(items_array) = index[key].as_array() {
            tracing::debug!(
                "Found {} items in JSON key '{}' for content type '{}' language '{}'",
                items_array.len(),
                key,
                content_type,
                language
            );

            for item in items_array {
                // Parse as UnifiedContentItem using the existing from_json method
                if let Some(unified_item) =
                    crate::content::UnifiedContentItem::from_json(item, content_type, language)
                {
                    content_index.items.push(unified_item);
                } else {
                    tracing::warn!("🔍 PARSE DEBUG: Failed to parse item as UnifiedContentItem");
                    tracing::debug!("🔍 PARSE DEBUG: Failed item JSON: {:?}", item);
                }
            }
            break; // Use the first key that has data
        }
    }

    // Cache the loaded index for future requests
    let version = format!("v{}", current_timestamp());
    GLOBAL_CONTENT_CACHE.cache_content(cache_key, &content_index, version);
    tracing::debug!(
        "📦 Cache STORE: {} index for '{}' cached for future use",
        content_type,
        language
    );

    Ok(content_index)
}

fn load_embedded_content(
    _config: &crate::config::ContentManageConfig,
    content_type: &str,
    language: &str,
    filename: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    use crate::resources::load;

    // Only content index.json files are embedded, not filter-index.json files
    // For filter-index.json, return error immediately to force filesystem/HTTP
    // fallback
    if filename == "filter-index.json" {
        return Err("Filter index not available in embedded resources".into());
    }

    // Use the specific resource key format that matches the build script
    let resource_key = format!("{content_type}_{language}_index");

    tracing::debug!(
        "Attempting to load embedded content with key: '{}'",
        resource_key
    );
    tracing::debug!(
        "Available content resources: {:?}",
        load::available_content_resources()
    );

    // Try to load from the content resources
    let result = load::content(&resource_key);

    match &result {
        Ok(content) => {
            tracing::debug!(
                "Successfully loaded embedded content for key '{}': {} characters",
                resource_key,
                content.len()
            );
        }
        Err(e) => {
            tracing::debug!(
                "Failed to load embedded content for key '{}': {}",
                resource_key,
                e
            );
        }
    }

    result.map_err(|e| {
        format!("Failed to load embedded content for key '{resource_key}': {e}").into()
    })
}

pub fn load_markdown_content(path: &str) -> Result<String, Box<dyn std::error::Error>> {
    // Try to load from embedded resources first (WASM compatible)
    if let Ok(content) = load_embedded_markdown_content(path) {
        tracing::debug!(
            "Successfully loaded embedded markdown content for: {}",
            path
        );
        return Ok(content);
    }

    // Fallback to filesystem for server-side rendering
    #[cfg(not(target_arch = "wasm32"))]
    {
        use std::fs;
        use std::path::Path;

        tracing::debug!("Trying to load markdown content from filesystem: {}", path);

        let file_path = Path::new(path);
        if !file_path.exists() {
            tracing::error!("File not found: {}", path);
            return Err(format!("File not found: {path}").into());
        }

        match fs::read_to_string(path) {
            Ok(content) => {
                tracing::debug!(
                    "Successfully loaded {} characters from {}",
                    content.len(),
                    path
                );
                Ok(content)
            }
            Err(e) => {
                tracing::error!("Failed to read file {}: {}", path, e);
                Err(format!("Failed to read file {path}: {e}").into())
            }
        }
    }

    // For WASM when embedded resources fail, return empty content (SSR handles
    // actual content)
    #[cfg(target_arch = "wasm32")]
    {
        tracing::debug!(
            "WASM: Embedded content not found for {}, returning empty (SSR handles content)",
            path
        );
        Ok(String::new())
    }
}

fn load_embedded_markdown_content(path: &str) -> Result<String, Box<dyn std::error::Error>> {
    use std::path::Path;

    use crate::resources::load;

    // Extract content type, language, and filename from path
    // Expected path format: content/{content_type}/{language}/filename.md
    let path_obj = Path::new(path);
    let components: Vec<_> = path_obj.components().collect();

    if components.len() < 4 {
        return Err("Invalid path format for embedded content".into());
    }

    // Extract parts from path
    let content_type = components[components.len() - 3]
        .as_os_str()
        .to_str()
        .ok_or("Invalid content type")?;
    let language = components[components.len() - 2]
        .as_os_str()
        .to_str()
        .ok_or("Invalid language")?;
    let filename = path_obj
        .file_stem()
        .and_then(|s| s.to_str())
        .ok_or("Invalid filename")?;

    // Create resource key: content_type_language_filename
    let resource_key = format!("{content_type}_{language}_{filename}");

    tracing::debug!(
        "Attempting to load embedded markdown with key: '{}'",
        resource_key
    );
    tracing::debug!(
        "Available content resources: {:?}",
        load::available_content_resources()
    );

    // Try to load from the content resources
    let result = load::content(&resource_key);

    match &result {
        Ok(content) => {
            tracing::debug!(
                "Successfully loaded embedded markdown for key '{}': {} characters",
                resource_key,
                content.len()
            );
        }
        Err(e) => {
            tracing::debug!(
                "Failed to load embedded markdown for key '{}': {}",
                resource_key,
                e
            );
        }
    }

    result.map_err(|e| {
        format!("Failed to load embedded markdown for key '{resource_key}': {e}").into()
    })
}

/// Load content by slug using content index with source_file path
pub fn load_content_by_slug(
    slug: &str,
    content_type: &str,
    language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    tracing::debug!(
        "Loading {} content by slug '{}' for language '{}'",
        content_type,
        slug,
        language
    );

    // Load the content index to find the item
    let index = load_content_index(content_type, language)?;

    // Find the item by slug first, then fallback to ID
    let item = index
        .items
        .iter()
        .find(|item| item.slug == slug || item.id == slug)
        .ok_or_else(|| {
            format!(
                "Content item with slug or ID '{}' not found in {} index",
                slug, content_type
            )
        })?;

    tracing::debug!(
        "Found content item: id='{}', source_file='{}'",
        item.id,
        item.source_file
    );

    // For loading source markdown files, use SITE_CONTENT_PATH (not server content
    // root)
    let source_content_root =
        std::env::var("SITE_CONTENT_PATH").unwrap_or_else(|_| "site/content".to_string());

    // Build the full path to the markdown file.
    //
    // Two layouts are supported:
    //   Category layout:  {root}/{type}/{lang}/{category}/{id}.md
    //   Page-bundle flat: {root}/{type}/{lang}/{id}/index.md
    //
    // Category is taken from item.categories[0]. When that value is empty
    // (e.g. projects with flat folder structure) the category segment is
    // omitted and the page-bundle path is tried first.
    let category = item
        .categories
        .first()
        .filter(|c| !c.is_empty())
        .cloned()
        .unwrap_or_default();

    let source_filename = if !item.source_file.is_empty() {
        if item.source_file.ends_with(".md") {
            item.source_file.clone()
        } else {
            format!("{}.md", item.source_file)
        }
    } else {
        // source_file not set: use item.id (= filename stem on disk).
        // Never use `slug` here — slug is the URL segment and diverges from
        // the filename for translated or descriptive slugs (e.g.
        // id="rust-web-development-2024",
        // slug="modern-rust-web-development-in-2024").
        format!("{}.md", item.id)
    };

    let lang_dir = std::path::PathBuf::from(&source_content_root)
        .join(content_type)
        .join(language);

    // Resolve the content path, trying page-bundle layout first when no
    // category is present.
    let content_path = if category.is_empty() {
        // Flat / page-bundle: try {id}/index.md first, then {id}.md
        let bundle_path = lang_dir.join(&item.id).join("index.md");
        if bundle_path.exists() {
            bundle_path
        } else {
            lang_dir.join(&source_filename)
        }
    } else {
        // Category layout: {category}/{source_filename}
        lang_dir.join(&category).join(&source_filename)
    };

    tracing::debug!(
        "Loading markdown content from path: {} (content_type={}, language={}, category={}, \
         source_file={})",
        content_path.to_string_lossy(),
        content_type,
        language,
        category,
        item.source_file
    );

    load_markdown_content(&content_path.to_string_lossy())
}

/// Get directory name for content type, WASM-compatible
fn get_content_directory(content_type: &str) -> Result<String, Box<dyn std::error::Error>> {
    // Try using the registry for server-side rendering (when not in WASM)
    #[cfg(not(target_arch = "wasm32"))]
    {
        use crate::content::traits::ContentKindRegistry;
        let registry = crate::create_content_kind_registry();
        match registry.kinds.get(content_type) {
            Some(_config) => Ok(content_type.to_string()),
            None => {
                // Fallback: use content_type as directory name
                tracing::warn!(
                    "Content type '{}' not found in registry, using as directory name",
                    content_type
                );
                Ok(content_type.to_string())
            }
        }
    }
    #[cfg(target_arch = "wasm32")]
    {
        // For WASM, use content_type as directory name directly
        Ok(content_type.to_string())
    }
}

/// Generic content loader that uses content-kinds configuration
/// This replaces hardcoded content type functions
pub fn load_content_by_type(
    content_type: &str,
    slug: &str,
    language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    // Load the content index to find the source_file for this slug
    let index = load_content_index(content_type, language)?;

    // Find the item by slug first, then fallback to ID
    let item = index
        .items
        .iter()
        .find(|item| item.slug == slug || item.id == slug)
        .ok_or_else(|| {
            format!(
                "Content item with slug or ID '{}' not found in {} index",
                slug, content_type
            )
        })?;

    let content_config = crate::config::ContentManageConfig::from_env();

    // Build the path using the source_file from the index (includes category
    // directory)
    let content_path = content_config.root_path.join(&item.source_file);

    tracing::debug!(
        "Generic content loader: type='{}', slug='{}', language='{}', path='{}', source_file='{}'",
        content_type,
        slug,
        language,
        content_path.to_string_lossy(),
        item.source_file
    );

    load_markdown_content(&content_path.to_string_lossy())
}

/// Cache management functions for unified content loading.
/// Provides centralized cache control.
/// Get cache statistics from the global content cache
pub fn get_content_cache_stats() -> crate::cache::content::ContentCacheStatistics {
    GLOBAL_CONTENT_CACHE.get_stats()
}

/// Clear all content caches
pub fn clear_content_cache() {
    GLOBAL_CONTENT_CACHE.clear_all();
    tracing::info!("🧹 Global content cache cleared");
}

/// Clear cache for specific content type
pub fn invalidate_content_type_cache(content_type: &str) {
    GLOBAL_CONTENT_CACHE.invalidate_content_type(content_type);
    tracing::info!("🧹 Cache invalidated for content type: {}", content_type);
}

/// Cleanup expired cache entries
pub fn cleanup_expired_content_cache() {
    GLOBAL_CONTENT_CACHE.cleanup_expired();
    tracing::debug!("🧹 Expired content cache entries cleaned up");
}

/// Async fetch for content index via HTTP (WASM client only, not SSR)
#[cfg(all(target_arch = "wasm32", not(feature = "ssr")))]
fn fetch_content_index_http(
    content_type: &str,
    language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    use std::sync::Mutex;

    use wasm_bindgen_futures::spawn_local;

    // For synchronous API, we need to return an error and let the calling component
    // handle async loading This is because we can't make synchronous HTTP
    // requests in WASM
    tracing::debug!(
        "WASM HTTP fallback: fetching /r/{}/{}/index.json",
        content_type,
        language
    );

    // Trigger async loading in background
    let content_type_owned = content_type.to_string();
    let language_owned = language.to_string();

    spawn_local(async move {
        tracing::debug!(
            "HTTP fetch: /r/{}/{}/index.json",
            content_type_owned,
            language_owned
        );

        match fetch_content_index_async(&content_type_owned, &language_owned).await {
            Ok(content) => {
                tracing::debug!(
                    "HTTP fetch succeeded ({} bytes): {}/{}",
                    content.len(),
                    content_type_owned,
                    language_owned
                );

                // Cache the result for future use
                let cache_key =
                    ContentCacheKey::new(&format!("{}_index", content_type_owned), &language_owned)
                        .with_language(language_owned.clone());
                if let Ok(content_index) = serde_json::from_str::<ContentIndex>(&content) {
                    let item_count = content_index.items.len();
                    GLOBAL_CONTENT_CACHE.cache_content(
                        cache_key,
                        content_index,
                        "http".to_string(),
                    );
                    tracing::debug!(
                        "Cached content index for {}/{}: {} posts",
                        content_type_owned,
                        language_owned,
                        item_count
                    );
                }
            }
            Err(e) => {
                tracing::warn!(
                    "HTTP fetch failed for {}/{}: {}",
                    content_type_owned,
                    language_owned,
                    e
                );
            }
        }
    });

    // Return empty content index as fallback - the async request will populate
    // cache
    let fallback_index = format!(
        r#"{{"items": [], "meta": {{"total": 0, "language": "{}", "content_type": "{}"}}, "pagination": {{"current_page": 1, "total_pages": 0, "per_page": 10}}}}"#,
        language, content_type
    );

    Ok(fallback_index)
}

/// Async HTTP fetch for content index (WASM only)
#[cfg(target_arch = "wasm32")]
async fn fetch_content_index_async(content_type: &str, language: &str) -> Result<String, String> {
    use wasm_bindgen::JsCast;
    use wasm_bindgen_futures::JsFuture;
    use web_sys::{Request, RequestInit, RequestMode, Response};

    let url = format!("/r/{}/{}/index.json", content_type, language);

    let opts = RequestInit::new();
    opts.set_method("GET");
    opts.set_mode(RequestMode::SameOrigin);

    let request = Request::new_with_str_and_init(&url, &opts)
        .map_err(|e| format!("Failed to create request: {:?}", e))?;

    let window = web_sys::window().ok_or("No window available")?;
    let resp_value = JsFuture::from(window.fetch_with_request(&request))
        .await
        .map_err(|e| format!("Fetch failed: {:?}", e))?;

    let resp: Response = resp_value
        .dyn_into()
        .map_err(|_| "Failed to cast to Response")?;

    if !resp.ok() {
        return Err(format!("HTTP error: {}", resp.status()));
    }

    let text = JsFuture::from(
        resp.text()
            .map_err(|e| format!("Failed to get text: {:?}", e))?,
    )
    .await
    .map_err(|e| format!("Text conversion failed: {:?}", e))?;

    let content_str = text.as_string().ok_or("Failed to convert to string")?;

    Ok(content_str)
}

/// SSR filesystem read for content index - no HTTP requests needed in SSR
#[cfg(not(target_arch = "wasm32"))]
fn fetch_content_index_http_ssr(
    content_type: &str,
    language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    // For SSR, read directly from filesystem using environment variables
    use std::env;
    let server_content_root =
        env::var("SITE_SERVER_CONTENT_ROOT").unwrap_or_else(|_| "target/site/r".to_string());
    let file_path = format!(
        "{}/{}/{}/index.json",
        server_content_root, content_type, language
    );

    tracing::debug!("SSR: Reading content from filesystem: {}", file_path);

    match std::fs::read_to_string(&file_path) {
        Ok(content) => {
            tracing::debug!(
                "SSR: Successfully read {} bytes from {}",
                content.len(),
                file_path
            );
            Ok(content)
        }
        // A missing index means this content kind simply has no published
        // collection yet (e.g. a declared-but-empty kind, or content not yet
        // generated). Return an empty index so the grid renders empty instead
        // of failing the whole page. Other IO errors (permissions, etc.) still
        // propagate as real errors.
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            // A missing index is a graceful, expected case (declared-but-empty kind,
            // or a page route probed for content) — not a warning. Debug, not warn;
            // genuine IO errors still propagate via the `error!` branch below.
            tracing::debug!(
                "SSR: index.json absent at {} — rendering empty {} grid",
                file_path,
                content_type
            );
            Ok(format!(
                r#"{{"items":[],"pages":[],"language":"{}","content_type":"{}"}}"#,
                language, content_type
            ))
        }
        Err(e) => {
            tracing::error!("SSR: Failed to read file {}: {}", file_path, e);
            Err(format!("Failed to read index.json from {}: {}", file_path, e).into())
        }
    }
}

/// Unified filter index loader with integrated caching
/// Follows the same pattern as load_content_index for consistency
pub fn load_filter_index(
    content_type: &str,
    language: &str,
) -> Result<super::models::FilterIndex, Box<dyn std::error::Error>> {
    // Try to get from cache first
    let cache_key = ContentCacheKey::new(format!("{}_filter_index", content_type), language)
        .with_language(language.to_string());

    if let Some(cached_value) = GLOBAL_CONTENT_CACHE.get_content(&cache_key) {
        // Deserialize from JSON value back to FilterIndex
        if let Ok(cached_index) =
            serde_json::from_value::<super::models::FilterIndex>((*cached_value).clone())
        {
            tracing::debug!(
                "📦 Cache HIT: {} filter index for '{}' loaded from cache",
                content_type,
                language
            );
            return Ok(cached_index);
        }
    }

    tracing::debug!(
        "📦 Cache MISS: Loading {} filter index for '{}' from source",
        content_type,
        language
    );
    let config = crate::config::ContentManageConfig::from_env();

    // Try to load from embedded resources first (WASM compatible)
    let content = load_embedded_content(&config, content_type, language, "filter-index.json")
        .or_else(|embedded_err| {
            tracing::debug!(
                "Embedded filter index loading failed for {} {}: {}",
                content_type,
                language,
                embedded_err
            );

            #[cfg(target_arch = "wasm32")]
            {
                // In WASM environment, we can't access filesystem so return error immediately
                // This will be handled by the caller to make an HTTP request instead
                Err::<String, _>(format!(
                    "WASM: filesystem access not supported, need HTTP request fallback"
                ))
            }

            #[cfg(not(target_arch = "wasm32"))]
            {
                // For SSR, read from filesystem directly
                tracing::debug!(
                    "SSR: Reading filter index from filesystem for {}/{}",
                    content_type,
                    language
                );
                use std::env;
                let public_path =
                    env::var("SITE_PUBLIC_PATH").unwrap_or_else(|_| "public".to_string());
                let server_root_content =
                    env::var("SITE_SERVER_ROOT_CONTENT").unwrap_or_else(|_| "r".to_string());
                let file_path = format!(
                    "{}/{}/{}/{}/filter-index.json",
                    public_path, server_root_content, content_type, language
                );

                std::fs::read_to_string(&file_path).map_err(|e| {
                    format!("Failed to read filter-index.json from {}: {}", file_path, e)
                })
            }
        });

    // Second fallback: HTTP request for WASM
    #[cfg(target_arch = "wasm32")]
    let content = content.or_else(|file_err| {
        tracing::debug!("All local loading methods failed: {}", file_err);
        tracing::debug!(
            "Attempting HTTP request fallback for filter index {}/{}",
            content_type,
            language
        );
        fetch_filter_index_http(content_type, language)
    });

    let content = content?;

    tracing::debug!(
        "{} filter index loaded for {}: {} characters",
        content_type,
        language,
        content.len()
    );

    let filter_index: super::models::FilterIndex = serde_json::from_str(&content)?;

    // Cache the loaded filter index
    let version = format!("v{}", current_timestamp());
    GLOBAL_CONTENT_CACHE.cache_content(cache_key, &filter_index, version);

    tracing::debug!(
        "📦 Cache STORED: {} filter index for '{}' cached successfully",
        content_type,
        language
    );

    Ok(filter_index)
}

/// WASM HTTP fetch for filter index.
///
/// Returns a structurally valid empty `FilterIndex` immediately (so callers
/// get a usable value without blocking) and fires a background `spawn_local`
/// task that fetches the real data and inserts it into `GLOBAL_CONTENT_CACHE`.
/// On the next render cycle the cache will be warm and callers will get the
/// real data.
#[cfg(target_arch = "wasm32")]
fn fetch_filter_index_http(
    content_type: &str,
    language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    use wasm_bindgen_futures::spawn_local;

    web_sys::console::debug_1(
        &format!(
            "WASM filter-index: scheduling background fetch for {}/{}",
            content_type, language
        )
        .into(),
    );

    let content_type_owned = content_type.to_string();
    let language_owned = language.to_string();

    spawn_local(async move {
        match fetch_filter_index_async(&content_type_owned, &language_owned).await {
            Ok(json) => {
                web_sys::console::debug_1(
                    &format!(
                        "WASM filter-index: fetched {} bytes for {}/{}",
                        json.len(),
                        content_type_owned,
                        language_owned
                    )
                    .into(),
                );

                if let Ok(filter_index) = serde_json::from_str::<super::models::FilterIndex>(&json)
                {
                    let cache_key = ContentCacheKey::new(
                        format!("{}_filter_index", content_type_owned),
                        &language_owned,
                    )
                    .with_language(language_owned.clone());

                    let version = format!("v{}", crate::cache::current_timestamp());
                    GLOBAL_CONTENT_CACHE.cache_content(cache_key, &filter_index, version);

                    web_sys::console::debug_1(
                        &format!(
                            "WASM filter-index: cached {}/{} ({} categories, {} tags)",
                            content_type_owned,
                            language_owned,
                            filter_index.categories.len(),
                            filter_index.tags.len(),
                        )
                        .into(),
                    );
                } else {
                    web_sys::console::warn_1(
                        &format!(
                            "WASM filter-index: failed to deserialize response for {}/{}",
                            content_type_owned, language_owned
                        )
                        .into(),
                    );
                }
            }
            Err(e) => {
                web_sys::console::warn_1(
                    &format!(
                        "WASM filter-index: HTTP fetch failed for {}/{}: {}",
                        content_type_owned, language_owned, e
                    )
                    .into(),
                );
            }
        }
    });

    // Return a structurally valid empty FilterIndex while the real data loads.
    // `categories` and `tags` are `HashMap<String, _>` — must be `{}` not `[]`.
    let fallback = format!(
        r#"{{"generated_at":"","content_type":"{}","language":"{}","total_posts":0,"categories":{{}},"tags":{{}}}}"#,
        content_type, language
    );

    Ok(fallback)
}

/// Async inner fetch used by `fetch_filter_index_http` (WASM only).
#[cfg(target_arch = "wasm32")]
async fn fetch_filter_index_async(content_type: &str, language: &str) -> Result<String, String> {
    // Mirror the SSR file layout: static files are served under /r/
    let url = format!("/r/{}/{}/filter-index.json", content_type, language);

    let response = reqwasm::http::Request::get(&url)
        .send()
        .await
        .map_err(|e| format!("fetch {}: {}", url, e))?;

    if response.status() != 200 {
        return Err(format!("HTTP {} from {}", response.status(), url));
    }

    response
        .text()
        .await
        .map_err(|e| format!("read body from {}: {}", url, e))
}

#[cfg(not(target_arch = "wasm32"))]
fn fetch_filter_index_http(
    _content_type: &str,
    _language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    Err("HTTP fetch not available on server".into())
}

// Stub for when the real WASM fetch function is not available (SSR or non-WASM)
#[cfg(any(not(target_arch = "wasm32"), feature = "ssr"))]
fn fetch_content_index_http(
    _content_type: &str,
    _language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    Err("HTTP fetch not available on server or during SSR".into())
}

/// Synchronously read content index JSON from DOM element embedded by SSR
/// shell.rs. The element id is `content-data-{content_type}-{language}` and
/// contains the serialized `ContentIndex` JSON injected by the server into
/// `<head>`. This prevents WASM hydration mismatches where SSR renders N items
/// but WASM starts with 0.
#[cfg(target_arch = "wasm32")]
fn try_load_content_from_dom(
    content_type: &str,
    language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    use web_sys::window;

    let element_id = format!("content-data-{}-{}", content_type, language);
    tracing::debug!("try_load_content_from_dom: looking for #{}", element_id);

    let win = window().ok_or("no window")?;
    let doc = win.document().ok_or("no document")?;
    let elem = doc
        .get_element_by_id(&element_id)
        .ok_or_else(|| format!("element #{} not found in DOM", element_id))?;

    // Try text_content first (for text nodes inside script), then inner_html
    // as Leptos uses inner_html prop which may render differently across browsers.
    let text = elem
        .text_content()
        .filter(|s| !s.is_empty())
        .or_else(|| {
            let ih = elem.inner_html();
            if ih.is_empty() {
                None
            } else {
                Some(ih)
            }
        })
        .ok_or_else(|| format!("element #{} is empty", element_id))?;

    tracing::debug!(
        "DOM content-data found: #{} ({} bytes)",
        element_id,
        text.len()
    );
    Ok(text)
}

#[cfg(not(target_arch = "wasm32"))]
fn try_load_content_from_dom(
    _content_type: &str,
    _language: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    Err("DOM not available outside WASM".into())
}
