use dioxus::prelude::ServerFnError;
use dioxus::prelude::*;
use polymodel_api::space_polymodel::library;

use crate::Route;
use crate::examples::{HERO_SAMPLE_HANDLES, first_hero_sample_handle};
use crate::profile::profile_href;
use crate::session::SessionIdentity;
use crate::thing_card::ThingCard;
use crate::thing_detail::thing_detail_href;

/// Cookie set client-side when the logged-out hero band is dismissed; read
/// server-side so SSR omits the band on return without a hydration flash.
const HERO_DISMISS_COOKIE: &str = "pm_hero_dismissed";

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FeedAlgorithm {
    Recent,
    Hot,
    Following,
}

impl FeedAlgorithm {
    fn as_str(self) -> &'static str {
        match self {
            Self::Recent => "recent",
            Self::Hot => "hot",
            Self::Following => "following",
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Recent => "Recent",
            Self::Hot => "Hot",
            Self::Following => "Following",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum BrowseFeedState {
    Loading,
    Error(String),
    Empty,
    Populated(Vec<library::ThingViewBasic>),
}

fn browse_feed_state(
    pending: bool,
    result: Option<&Result<Vec<library::ThingViewBasic>, String>>,
) -> BrowseFeedState {
    if pending && result.is_none() {
        return BrowseFeedState::Loading;
    }

    match result {
        Some(Ok(things)) if things.is_empty() => BrowseFeedState::Empty,
        Some(Ok(things)) => BrowseFeedState::Populated(things.clone()),
        Some(Err(error)) => BrowseFeedState::Error(error.clone()),
        None => BrowseFeedState::Loading,
    }
}

/// Whether the logged-out hero band should render. It shows only to a
/// definitively anonymous visitor who has not dismissed it; an unresolved
/// session (`Unknown`), a server-evaluated dismissal seed of `true`, or a
/// live client-read dismissal cookie each suppress it.
///
/// `hero_dismissed_seed` is the SSR/first-paint decision (server cookie read,
/// cached through hydration), so the band is correct in the initial markup
/// with no flash. `client_dismissed` is the post-hydration backstop: a fresh
/// read of the live cookie that keeps the band suppressed across client-side
/// navigation, where the cached seed would otherwise still read `false`.
fn should_show_hero(
    session: &SessionIdentity,
    hero_dismissed_seed: Option<&Result<bool, ServerFnError>>,
    client_dismissed: bool,
) -> bool {
    !client_dismissed
        && matches!(session, SessionIdentity::Anonymous)
        && !matches!(hero_dismissed_seed, Some(Ok(true)))
}

/// True when the cookie header carries the named flag (presence only; the
/// dismiss control always sets it to `1`). Pure so it stays unit-testable.
// Only reached from the wasm client read and the unit tests; on native
// non-test builds it is legitimately unreferenced.
#[cfg_attr(not(any(target_arch = "wasm32", test)), allow(dead_code))]
fn cookie_has_flag(cookie_header: &str, name: &str) -> bool {
    cookie_header
        .split(';')
        .filter_map(|pair| pair.split_once('='))
        .any(|(key, _)| key.trim() == name)
}

/// Read the dismissal cookie from the browser on the client. Returns `false`
/// on the server (where SSR's `hero_dismissed_seed` owns the decision) and on
/// non-wasm targets.
#[cfg(target_arch = "wasm32")]
fn client_cookie_dismissed() -> bool {
    use wasm_bindgen::JsValue;

    let Some(document) = web_sys::window().and_then(|window| window.document()) else {
        return false;
    };
    // `Document::cookie` lives behind the `HtmlDocument` web-sys feature, which
    // isn't enabled; read the property reflectively to avoid widening features.
    let cookie = js_sys::Reflect::get(document.as_ref(), &JsValue::from_str("cookie"))
        .ok()
        .and_then(|value| value.as_string())
        .unwrap_or_default();
    cookie_has_flag(&cookie, HERO_DISMISS_COOKIE)
}

#[cfg(not(target_arch = "wasm32"))]
fn client_cookie_dismissed() -> bool {
    false
}

#[allow(clippy::useless_format)]
#[component]
pub(crate) fn Browse() -> Element {
    let selected_algorithm = use_signal(|| FeedAlgorithm::Hot);

    let mut feed = use_server_future(move || {
        let algorithm = selected_algorithm.read().as_str().to_owned();
        feed_seed(algorithm, 24)
    })?;

    let feed_result = feed.read();
    let mapped = feed_result.as_ref().map(|r| match r {
        Ok(things) => Ok(things.clone()),
        Err(ServerFnError::ServerError { message, .. }) => Err(message.clone()),
        Err(other) => Err(other.to_string()),
    });
    let feed_state = browse_feed_state(feed.pending(), mapped.as_ref());

    // Show the logged-out hero band only to definitively anonymous visitors who
    // haven't dismissed it. Both inputs resolve server-side during SSR (session
    // via the App seed, dismissal via the cookie server fn), so the band is
    // present or absent in the initial markup with no post-hydration flash.
    let session = use_context::<Signal<SessionIdentity>>();
    let hero_dismissed = use_server_future(hero_dismissed_seed)?;
    let client_dismissed = use_hook(client_cookie_dismissed);
    let show_hero = should_show_hero(&session.read(), hero_dismissed().as_ref(), client_dismissed);

    rsx! {
        main { class: "browse-page",

            if show_hero {
                HeroBand {}
            }

            section { class: "browse-feed", aria_label: "Catalog feed",
                div { class: "tab-row", role: "tablist", aria_label: "Feeds",
                    FeedTab { algorithm: FeedAlgorithm::Hot, selected_algorithm }
                    FeedTab { algorithm: FeedAlgorithm::Recent, selected_algorithm }
                    FeedTab { algorithm: FeedAlgorithm::Following, selected_algorithm }
                }

                section { class: "browse-results-grid", aria_label: "Browse results",
                    match feed_state {
                        BrowseFeedState::Loading => rsx! {
                            for index in 0..6 {
                                div { key: "skeleton-{index}", class: "state-card loading-state",
                                    span { class: "status-pill", "Loading" }
                                    div { class: "skeleton-line skeleton-wide" }
                                    div { class: "skeleton-line" }
                                    div { class: "skeleton-box" }
                                }
                            }
                        },
                        BrowseFeedState::Error(error) => rsx! {
                            div { class: "state-card error-state browse-state-card",
                                p { "{error}" }
                                button { class: "button button-secondary", onclick: move |_| feed.restart(), "Retry" }
                            }
                        },
                        BrowseFeedState::Empty => rsx! {
                            div { class: "state-card empty-state browse-state-card",
                                span { class: "status-pill status-muted", "Empty" }
                                if *selected_algorithm.read() == FeedAlgorithm::Following && matches!(&*session.read(), SessionIdentity::Anonymous) {
                                    h2 { "Sign in to build a following feed" }
                                    p { "Follow makers to collect their latest Polymodel projects here." }
                                    form { class: "hero-band-signin", action: "/oauth/start", method: "get",
                                        input { r#type: "hidden", name: "return_to", value: "/" }
                                        button { class: "button button-primary", r#type: "submit", "Sign in" }
                                    }
                                } else if *selected_algorithm.read() == FeedAlgorithm::Following {
                                    h2 { "No followed projects yet" }
                                    p { "Follow makers from their profiles and their projects will appear here." }
                                } else {
                                    h2 { "No projects yet" }
                                    p { "Polymodel reached the appview feed, but this local index has no projects for the selected feed yet." }
                                }
                            }
                        },
                        BrowseFeedState::Populated(things) => rsx! {
                            for thing in things {
                                BrowseThingCard { key: "{thing.uri}", thing }
                            }
                        },
                    }
                }
            }
        }
    }
}

#[component]
fn BrowseThingCard(thing: library::ThingViewBasic) -> Element {
    let detail_href = thing_detail_href(&thing.uri);
    let creator_href = profile_href(&thing.author);
    rsx! { ThingCard { thing, detail_href, creator_href } }
}

#[component]
fn FeedTab(algorithm: FeedAlgorithm, selected_algorithm: Signal<FeedAlgorithm>) -> Element {
    let is_active = *selected_algorithm.read() == algorithm;
    let class = if is_active { "tab is-active" } else { "tab" };

    rsx! {
        button { class, role: "tab", aria_selected: "{is_active}", onclick: move |_| selected_algorithm.set(algorithm), "{algorithm.label()}" }
    }
}

#[component]
fn HeroBand() -> Element {
    // Rendered as raw HTML so the dismiss control carries a literal `onclick`
    // that fires before (or without) wasm hydration. A Dioxus event handler
    // would require the wasm bundle to be live, defeating the no-flash goal.
    //
    // Hiding via `style.display` rather than removing the node keeps Dioxus
    // hydration reconciliation intact: `style` is an attribute Dioxus never
    // rendered here, so re-renders of this same node (feed loads, tab
    // switches) won't clobber the hidden state. A *remount* (client-side
    // navigation back to `/` builds a fresh node) does drop the inline style,
    // so the cookie this sets is also read client-side in `should_show_hero`
    // to keep a dismissed band suppressed across navigation.
    let dismiss_html = format!(
        "<button type=\"button\" class=\"hero-dismiss\" aria-label=\"Dismiss introduction\" \
         onclick=\"document.cookie='{cookie}=1; path=/; max-age=31536000; samesite=lax'; \
         var b=this.closest('.hero-band'); if(b){{b.style.display='none';}}\">&times;</button>",
        cookie = HERO_DISMISS_COOKIE
    );
    let sample_handles = serde_json::to_string(HERO_SAMPLE_HANDLES)
        .expect("hero sample handles should serialize as JSON");
    let rotation_js = format!(
        r#"
        (() => {{
            const samples = {sample_handles};
            const input = document.currentScript.closest('.hero-band')?.querySelector('.hero-band-input');
            if (!input || samples.length < 2) return;
            let index = Math.floor(Math.random() * samples.length);
            input.placeholder = samples[index];
            window.setInterval(() => {{
                index = (index + 1) % samples.length;
                input.placeholder = samples[index];
            }}, 30000);
        }})();
        "#,
    );

    rsx! {
        section { class: "hero-band blueprint-panel", aria_label: "About Polymodel",
            div { class: "hero-band-body",
                p { class: "hero-band-pitch",
                    "Share your creations. Make things, remix them. Without getting locked in to a closed platform."
                }
                div { class: "hero-band-actions",
                    form { class: "hero-band-signin", action: "/oauth/start", method: "get",
                        input { r#type: "hidden", name: "return_to", value: "/" }
                        input {
                            class: "hero-band-input",
                            r#type: "text",
                            name: "identifier",
                            placeholder: "{first_hero_sample_handle()}",
                            aria_label: "Bluesky handle",
                        }
                        button { class: "button button-secondary", r#type: "submit", "Sign in" }
                    }
                    Link { class: "hero-band-about", to: Route::About {}, "what's this?" }
                }
            }
            div { class: "hero-band-dismiss", dangerous_inner_html: dismiss_html }
            script { dangerous_inner_html: rotation_js }
        }
    }
}

#[server]
async fn hero_dismissed_seed() -> Result<bool, ServerFnError> {
    use axum_extra::extract::cookie::CookieJar;
    use dioxus::prelude::dioxus_fullstack::FullstackContext;

    let context = FullstackContext::current()
        .ok_or_else(|| ServerFnError::new("missing Dioxus fullstack request context"))?;
    let parts = context.parts_mut().clone();
    let jar = CookieJar::from_headers(&parts.headers);
    Ok(jar.get(HERO_DISMISS_COOKIE).is_some())
}

#[server]
async fn feed_seed(
    algorithm: String,
    limit: i64,
) -> Result<Vec<library::ThingViewBasic>, ServerFnError> {
    let feed = crate::appview::ssr::get_feed_from_fullstack_context(&algorithm, limit).await?;
    Ok(feed.items.into_iter().map(|item| item.thing).collect())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::thing_card::thing_card_fixtures;

    #[test]
    fn browse_state_distinguishes_loading_empty_error_and_populated() {
        assert_eq!(browse_feed_state(true, None), BrowseFeedState::Loading);
        assert_eq!(
            browse_feed_state(false, Some(&Ok(vec![]))),
            BrowseFeedState::Empty
        );
        assert_eq!(
            browse_feed_state(false, Some(&Err("boom".to_string()))),
            BrowseFeedState::Error("boom".to_string())
        );
        match browse_feed_state(false, Some(&Ok(thing_card_fixtures()))) {
            BrowseFeedState::Populated(things) => assert_eq!(things.len(), 3),
            state => panic!("expected populated state, got {state:?}"),
        }
    }

    #[test]
    fn hero_shows_only_for_anonymous_and_undismissed() {
        use crate::session::AuthenticatedIdentity;
        use jacquard_common::types::string::{Did, Handle};

        assert!(should_show_hero(&SessionIdentity::Anonymous, None, false));
        assert!(should_show_hero(
            &SessionIdentity::Anonymous,
            Some(&Ok(false)),
            false
        ));
        // Either dismissal signal suppresses the band: the SSR seed...
        assert!(!should_show_hero(
            &SessionIdentity::Anonymous,
            Some(&Ok(true)),
            false
        ));
        // ...or the live client-read cookie (e.g. after client-side navigation).
        assert!(!should_show_hero(
            &SessionIdentity::Anonymous,
            Some(&Ok(false)),
            true
        ));
        // An unresolved session must not flash the band before identity lands.
        assert!(!should_show_hero(&SessionIdentity::Unknown, None, false));

        let authed = SessionIdentity::Authenticated(AuthenticatedIdentity {
            avatar: None,
            did: Did::new_owned("did:plc:tester").unwrap(),
            handle: Handle::new_owned("tester.test").unwrap(),
            display_name: None,
        });
        assert!(!should_show_hero(&authed, None, false));
    }

    #[test]
    fn cookie_flag_detects_presence_only_among_other_cookies() {
        assert!(cookie_has_flag("pm_hero_dismissed=1", HERO_DISMISS_COOKIE));
        assert!(cookie_has_flag(
            "session=abc; pm_hero_dismissed=1; theme=dark",
            HERO_DISMISS_COOKIE
        ));
        assert!(!cookie_has_flag(
            "session=abc; theme=dark",
            HERO_DISMISS_COOKIE
        ));
        assert!(!cookie_has_flag("", HERO_DISMISS_COOKIE));
        // Must not match a cookie whose name merely contains the flag.
        assert!(!cookie_has_flag(
            "not_pm_hero_dismissed=1",
            HERO_DISMISS_COOKIE
        ));
    }
}
