//! PostViewer page component — unified for SSR + WASM
//!
//! Both SSR and WASM go through this single component to guarantee that the
//! rendered DOM structure is identical, enabling clean Leptos 0.7 hydration.
//!
//! **SSR**: `build_initial_content` loads the markdown synchronously via
//!   `load_content_by_slug` + `process_markdown_file` so the full post HTML is
//!   present in the first-paint HTML (SEO + no flash).
//!
//! **WASM**: starts with the base i18n content map, then an `Effect` fires,
//!   fetches the rendered content from `/api/content-render/…`, and updates
//!   the `RwSignal<HashMap>` → the `{move || …}` DynChild re-renders in place.
//!
//! Both sides emit the same `<!-->`-bounded DynChild structure, so the
//! hydration cursor is never surprised by a missing marker node.
//!
//! Language switching:
//!   After the first successful fetch the stable `id` (filename stem, same
//!   across all languages) is stored.  On `lang.current` change the id is used
//!   as the fetch slug — the server resolves it via `item.id == slug` —
//!   and the browser URL is updated to the canonical form for the new language
//!   via the route config template (handles `recipes` ↔ `recetas`).

#[cfg(not(target_arch = "wasm32"))]
use std::collections::HashMap;

use leptos::prelude::*;
use rustelo_core_lib::{config::get_default_language, state::use_language};
use serde::{Deserialize, Serialize};

use crate::common::use_page_i18n_dynamic;
use crate::post_viewer::path_resolver::PathResolver;

/// JSON shape returned by `GET /api/content-render/{type}/{lang}/{slug}`.
/// `id` is the stable filesystem identifier (same across all languages).
/// `category` is the primary category, invariant across languages.
/// `translation_slugs` maps language code → localized URL slug of the
/// translation. `translation_ids` maps language code → stable id of the
/// translation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostContentData {
    pub title: String,
    pub subtitle: String,
    pub html: String,
    #[serde(default)]
    pub id: String,
    #[serde(default)]
    pub category: String,
    #[serde(default)]
    pub author: String,
    #[serde(default)]
    pub date: String,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub translation_slugs: std::collections::HashMap<String, String>,
    #[serde(default)]
    pub translation_ids: std::collections::HashMap<String, String>,
}

/// Fetch rendered post content from the server (WASM only).
///
/// HTTP 401 triggers the registered unauthorized handler (clear auth + redirect
/// to login) via `rustelo_core_lib::utils::fetch::on_unauthorized`.
#[cfg(target_arch = "wasm32")]
async fn fetch_post_content(
    content_type: String,
    language: String,
    slug: String,
) -> Result<PostContentData, String> {
    let url = format!("/api/content-render/{}/{}/{}", content_type, language, slug);
    let resp = rustelo_core_lib::utils::fetch::guarded_get(&url, "application/json").await?;
    resp.json::<PostContentData>()
        .await
        .map_err(|e| e.to_string())
}

/// Build a canonical post URL from the route config template (WASM only).
///
/// Prefers `translation_slug` (localized URL slug for `target_lang`) over
/// the stable `id` so the resulting URL is fully localized.
/// Falls back to `id` when no localized slug is available.
#[cfg(target_arch = "wasm32")]
fn build_post_url_for_lang(
    content_type: &str,
    target_lang: &str,
    category: &str,
    id: &str,
    translation_slug: Option<&str>,
) -> Option<String> {
    let slug_in_url = translation_slug.unwrap_or(id);
    let config = rustelo_core_lib::routing::utils::load_routes_config();
    config
        .routes
        .iter()
        .find(|r| {
            r.get_enabled()
                && r.get_language() == Some(target_lang.to_string())
                && r.content_type.as_deref() == Some(content_type)
                && r.path.contains("{slug}")
        })
        .map(|route| {
            route
                .path
                .replace("{category}", category)
                .replace("{slug}", slug_in_url)
        })
}

/// Load post content synchronously on SSR for SEO.
///
/// Returns the base i18n map extended with `post-title`, `post-subtitle`,
/// and `post-content` from the rendered markdown.  On WASM this is never
/// called — the WASM path starts with the base map and updates asynchronously.
#[cfg(not(target_arch = "wasm32"))]
fn build_initial_content_ssr(
    base: HashMap<String, String>,
    slug: &str,
    content_type: &str,
    language: &str,
) -> HashMap<String, String> {
    use rustelo_core_lib::fluent::{
        load_content_by_slug, process_markdown_file, rewrite_content_image_paths,
    };

    let mut content = base;
    if slug.is_empty() {
        return content;
    }
    let Ok(markdown) = load_content_by_slug(slug, content_type, language) else {
        return content;
    };
    let Ok((metadata, html)) = process_markdown_file(&markdown) else {
        return content;
    };
    let html = rewrite_content_image_paths(&html, content_type, language, slug);
    let title = metadata.title.unwrap_or_else(|| {
        slug.replace('-', " ")
            .split_whitespace()
            .map(|word| {
                let mut chars = word.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => {
                        first.to_uppercase().collect::<String>() + &chars.collect::<String>()
                    }
                }
            })
            .collect::<Vec<_>>()
            .join(" ")
    });
    content.insert("post-title".into(), title);
    content.insert(
        "post-subtitle".into(),
        metadata.subtitle.unwrap_or_default(),
    );
    content.insert("post-content".into(), html);
    if let Some(author) = metadata.author {
        content.insert("post-author".into(), author);
    }
    if let Some(date) = metadata.date {
        content.insert("post-date".into(), date);
    }
    if !metadata.tags.is_empty() {
        content.insert("post-tags".into(), metadata.tags.join(", "));
    }
    content
}

/// Unified PostViewer page component — SSR and WASM share the same structure.
///
/// * **SSR**: loads markdown synchronously → full HTML in first paint (SEO)
/// * **WASM**: `Effect` + `spawn_local` fetch via `/api/content-render/…`
/// * Both sides emit the same `{move || …}` DynChild markers → hydration safe
#[component]
pub fn PostViewerPageClient(
    #[prop(default = get_default_language().to_string())] _language: String,
    #[prop(default = "".to_string())] slug: String,
    #[prop(default = "content".to_string())] content_type: String,
    #[prop(default = true)] show_author: bool,
    #[prop(default = true)] show_date: bool,
    #[prop(default = true)] show_tags: bool,
    #[prop(default = false)] show_related_posts: bool,
    #[prop(default = false)] enable_comments: bool,
    #[prop(default = false)] enable_social_sharing: bool,
) -> impl IntoView {
    let lang = use_language();
    let current_path = use_context::<ReadSignal<String>>();
    let _set_path = use_context::<WriteSignal<String>>();

    // i18n strings (navigation labels, etc.)
    let base_content = use_page_i18n_dynamic(vec!["post-viewer-"], || {
        rustelo_core_lib::get_all_content_types()
            .iter()
            .map(|ct| format!("{}-", ct))
            .collect()
    });

    // Initial content:
    //  SSR  — load markdown synchronously so search engines see real content.
    //  WASM — start with base i18n map; Effect below fills in post data async.
    let initial = {
        #[cfg(not(target_arch = "wasm32"))]
        {
            let lang_str = lang.current.get_untracked();
            build_initial_content_ssr(
                base_content.get_untracked(),
                &slug,
                &content_type,
                &lang_str,
            )
        }
        #[cfg(target_arch = "wasm32")]
        {
            base_content.get_untracked()
        }
    };

    let merged_content = RwSignal::new(initial);

    // Stores the stable content id, primary category, and translation slugs from
    // the last fetch.
    let (post_id, set_post_id) = signal(String::new());
    let (_post_category, set_post_category) = signal(String::new());
    // translation_slugs: lang → localized URL slug in that language.
    let (post_translation_slugs, set_post_translation_slugs) =
        signal(std::collections::HashMap::<String, String>::new());

    let slug_prop = slug.clone();
    let ct = content_type.clone();

    // WASM only: Effect re-runs whenever lang or path changes to fetch new content.
    // SSR does NOT use an Effect — content is already loaded synchronously above,
    // and Effect::new on SSR panics because no executor is available.
    #[cfg(target_arch = "wasm32")]
    {
        Effect::new(move |_| {
            let ctx_lang = lang.current.get();
            let raw_path = current_path.map(|p| p.get()).unwrap_or_default();
            let url_slug = if !raw_path.is_empty() {
                PathResolver::extract_parameters(&raw_path)
                    .slug
                    .unwrap_or_else(|| slug_prop.clone())
            } else {
                slug_prop.clone()
            };

            let stored_id = post_id.get_untracked();
            let fetch_slug = if !stored_id.is_empty() {
                stored_id.clone()
            } else {
                url_slug.clone()
            };

            // Navigate to canonical URL on language switch.
            if !stored_id.is_empty() && fetch_slug != url_slug {
                let stored_category = _post_category.get_untracked();
                // Prefer the localized translation slug for the target language if available.
                let t_slugs = post_translation_slugs.get_untracked();
                let localized_slug = t_slugs.get(&ctx_lang).map(|s| s.as_str());
                if let Some(new_url) = build_post_url_for_lang(
                    &ct,
                    &ctx_lang,
                    &stored_category,
                    &stored_id,
                    localized_slug,
                ) {
                    if new_url != raw_path {
                        if let Some(sp) = _set_path {
                            rustelo_core_lib::utils::nav::anchor_navigate(sp, &new_url);
                        }
                    }
                }
            }

            let ct_c = ct.clone();
            let base = base_content.get_untracked();

            leptos::task::spawn_local(async move {
                match fetch_post_content(ct_c, ctx_lang, fetch_slug).await {
                    Ok(post) => {
                        let mut m = base;
                        m.insert("post-title".into(), post.title.clone());
                        m.insert("post-subtitle".into(), post.subtitle.clone());
                        m.insert("post-content".into(), post.html.clone());
                        if !post.author.is_empty() {
                            m.insert("post-author".into(), post.author);
                        }
                        if !post.date.is_empty() {
                            m.insert("post-date".into(), post.date);
                        }
                        if !post.tags.is_empty() {
                            m.insert("post-tags".into(), post.tags.join(", "));
                        }
                        if !post.id.is_empty() {
                            set_post_id.set(post.id.clone());
                        }
                        if !post.category.is_empty() {
                            set_post_category.set(post.category);
                        }
                        if !post.translation_slugs.is_empty() {
                            set_post_translation_slugs.set(post.translation_slugs);
                        }
                        merged_content.set(m);
                    }
                    Err(e) => {
                        tracing::warn!("PostViewer fetch error: {}", e);
                    }
                }
            });
        });
    }

    // SSR: suppress unused warnings for WASM-only bindings.
    #[cfg(not(target_arch = "wasm32"))]
    let _ = (
        slug_prop,
        ct,
        post_id,
        set_post_id,
        _post_category,
        set_post_category,
        post_translation_slugs,
        set_post_translation_slugs,
    );

    let slug_for_view = slug.clone();
    let ct_for_view = content_type.clone();

    // DynChild (`{move || …}`) is the reactive gate for content updates.
    // BOTH SSR and WASM emit `<!-->`-bounded DynChild → hydration cursor matches.
    view! {
        {move || {
            let current_slug = current_path
                .and_then(|p| PathResolver::extract_parameters(&p.get()).slug)
                .unwrap_or_else(|| slug_for_view.clone());
            // SSR: get_untracked() — one-shot render, no reactive owner present.
            // WASM: get() — tracked so the DynChild re-renders when content updates.
            #[cfg(not(target_arch = "wasm32"))]
            let content_map = merged_content.get_untracked();
            #[cfg(target_arch = "wasm32")]
            let content_map = merged_content.get();
            view! {
                <crate::post_viewer::unified::UnifiedPostViewerPage
                    lang_content=content_map
                    slug=current_slug
                    content_type=ct_for_view.clone()
                    show_author=show_author
                    show_date=show_date
                    show_tags=show_tags
                    show_related_posts=show_related_posts
                    enable_comments=enable_comments
                    enable_social_sharing=enable_social_sharing
                />
            }
        }}
    }
}
