use dioxus::prelude::*;
use jacquard_common::xrpc::XrpcClient;
use polymodel_api::space_polymodel::{actor::get_session::GetSession, library};

mod auth;
mod client;
mod fonts;
mod mesh;
mod session;
mod thing_card;
mod viewer;

// Frontend compile-time configuration, generated by build.rs from POLYMODEL_*.
mod env;
// Server-only indexing infrastructure (Hydrant firehose + SQLite projection).
#[cfg(feature = "server")]
mod indexing;
// Server-only XRPC read endpoints (appview query layer, PM-26).
#[cfg(feature = "server")]
mod appview;

use auth::{ToastState, broadcast_sign_out, login_url, logout_url, navigate_to};
use client::PolymodelClient;
use session::SessionIdentity;
use thing_card::ThingCard;
use viewer::{ViewerPage, demo_model_for_route, demo_models};

const FAVICON: Asset = asset!("/assets/favicon.jpg");
const THEME_CSS: Asset = asset!("/assets/styling/theme.css");
const BASE_CSS: Asset = asset!("/assets/styling/base.css");
const CARDS_CSS: Asset = asset!("/assets/styling/cards.css");
const BROWSE_CSS: Asset = asset!("/assets/styling/browse.css");
const POLYMODEL_CSS: Asset = asset!("/assets/styling/polymodel.css");

#[cfg(feature = "server")]
fn main() {
    // The closure runs inside the server's Tokio runtime at startup, so the
    // indexing tasks spawned here live for the process lifetime alongside Axum.
    dioxus::serve(|| async {
        // Server runtime config: load .env, then read DATABASE_URL (and any
        // future server env) in one place. Hydrant reads its own HYDRANT_*.
        let _ = dotenvy::dotenv();
        let _ = tracing_subscriber::fmt()
            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
            .try_init();
        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); // Hydrant selects AWS-LC while other deps enable ring; choose before websocket TLS setup.
        let cfg = indexing::config::ServerConfig::load();
        let db = indexing::db::init_db(&cfg)
            .await
            .expect("failed to initialize sqlite projection database");
        indexing::start_indexing(db.clone())
            .await
            .expect("failed to start indexing pipeline");

        // The same SQLite pool backs both indexing and the appview read handlers.
        let app_state = std::sync::Arc::new(appview::state::AppState::new(db));
        Ok(dioxus::server::router(App).merge(appview::router(app_state)))
    });
}

#[cfg(not(feature = "server"))]
fn main() {
    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
    {
        console_error_panic_hook::set_once();
        tracing_wasm::set_as_global_default();
    }

    dioxus::launch(App);
}

#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
    #[route("/")]
    Browse {},
    #[route("/foundation")]
    Foundation {},
    #[route("/viewer/:model_id")]
    Viewer { model_id: String },
}

#[component]
fn App() -> Element {
    let client = use_context_provider(PolymodelClient::default);
    let session = use_context_provider(|| Signal::new(SessionIdentity::Anonymous));
    let toasts = use_context_provider(|| Signal::new(ToastState::default()));

    let _session_seed = use_resource(move || {
        let client = client.clone();
        let mut session = session;
        async move {
            match client.send(GetSession).await {
                Ok(response) => match response.into_output() {
                    Ok(output) => session.set(SessionIdentity::from_get_session(output)),
                    Err(error) => tracing::warn!(%error, "failed to decode Polymodel session"),
                },
                Err(error) => tracing::warn!(%error, "failed to fetch Polymodel session"),
            }
        }
    });

    rsx! {
        document::Meta { name: "viewport", content: "width=device-width, initial-scale=1" }
        document::Title { "Polymodel" }
        document::Link { rel: "icon", href: FAVICON }
        Stylesheet { href: THEME_CSS }
        Stylesheet { href: BASE_CSS }
        Stylesheet { href: CARDS_CSS }
        Stylesheet { href: BROWSE_CSS }
        Stylesheet { href: POLYMODEL_CSS }

        Router::<Route> {}
        ToastShelf { toasts }
    }
}

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

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

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

#[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,
    }
}

#[allow(clippy::useless_format)]
#[component]
fn Browse() -> Element {
    let viewer_model = demo_models()[0].id.to_string();
    let client = use_context::<PolymodelClient>();
    let session = use_context::<Signal<SessionIdentity>>();
    let toasts = use_context::<Signal<ToastState>>();
    let selected_algorithm = use_signal(|| FeedAlgorithm::Recent);

    let mut feed = use_resource(move || {
        let client = client.clone();
        let algorithm = *selected_algorithm.read();
        async move {
            let request = library::get_feed::GetFeed::new()
                .algorithm(Some(algorithm.as_str().into()))
                .limit(24)
                .build();
            let response = client
                .send(request)
                .await
                .map_err(|error| format!("Feed request failed: {error}"))?;
            let output = response
                .into_output()
                .map_err(|error| format!("Feed decode failed: {error}"))?;
            Ok(output
                .value
                .items
                .into_iter()
                .map(|item| item.thing)
                .collect::<Vec<_>>())
        }
    });

    let feed_result = feed.read();
    let feed_state = browse_feed_state(feed.pending(), feed_result.as_ref());

    rsx! {
        main { class: "browse-page product-shell",
            header { class: "product-header",
                div { class: "product-kicker", "ATProto model sharing" }
                div { class: "product-header-row",
                    div {
                        h1 { "Polymodel" }
                    }
                    nav { class: "product-nav", aria_label: "Primary",
                        a { href: "#browse-shell", "Browse" }
                        Link { to: Route::Foundation {}, "Foundation" }
                        Link {
                            class: "button button-secondary",
                            to: Route::Viewer { model_id: viewer_model },
                            "Open viewer"
                        }
                        SessionControl { session, toasts }
                    }
                }
            }

            section { id: "browse-shell", class: "browse-controls blueprint-panel", aria_label: "Browse shell",
                label { class: "search-field",
                    span { "Search model projects" }
                    input {
                        r#type: "search",
                        placeholder: "Search by project, tag, or maker",
                        aria_label: "Search model projects"
                    }
                }
                div { class: "tab-row", role: "tablist", aria_label: "Feeds",
                    FeedTab { algorithm: FeedAlgorithm::Recent, selected_algorithm }
                    FeedTab { algorithm: FeedAlgorithm::Hot, selected_algorithm }
                    button { class: "tab", role: "tab", aria_selected: "false", disabled: true, "Following" }
                }
            }

            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) => {
                        let error_message = error.clone();
                        rsx! {
                            div { class: "state-card error-state browse-state-card",
                                p { "{error_message}" }
                            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" }
                            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 {
                            ThingCard { key: "{thing.uri}", thing }
                        }
                    },
                }
            }
        }
    }
}

#[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 SessionControl(session: Signal<SessionIdentity>, toasts: Signal<ToastState>) -> Element {
    let identity = session.read().clone();
    let mut identifier = use_signal(String::new);

    match identity {
        SessionIdentity::Anonymous => rsx! {
            form {
                class: "session-control",
                onsubmit: move |_| navigate_to(&login_url(&identifier.read())),
                input {
                    r#type: "text",
                    placeholder: "handle.test",
                    aria_label: "ATProto handle",
                    value: "{identifier}",
                    oninput: move |event| identifier.set(event.value())
                }
                button { class: "button button-primary", r#type: "submit", "Sign in" }
            }
        },
        SessionIdentity::Authenticated(identity) => {
            let label = SessionIdentity::Authenticated(identity).label();
            rsx! {
                div { class: "session-control session-control-authenticated",
                    span { class: "status-pill", "{label}" }
                    button {
                        class: "button button-secondary",
                        onclick: move |_| {
                            broadcast_sign_out();
                            session.set(SessionIdentity::Anonymous);
                            toasts.write().push("Signing out…");
                            navigate_to(logout_url());
                        },
                        "Sign out"
                    }
                }
            }
        }
    }
}

#[component]
fn ToastShelf(toasts: Signal<ToastState>) -> Element {
    let items = toasts.read().items().to_vec();

    rsx! {
        div { class: "toast-shelf", aria_live: "polite",
            for toast in items {
                div { key: "toast-{toast.id}", class: "toast blueprint-panel",
                    span { "{toast.message}" }
                    button {
                        class: "toast-dismiss",
                        aria_label: "Dismiss notification",
                        onclick: move |_| toasts.write().dismiss(toast.id),
                        "×"
                    }
                }
            }
        }
    }
}

#[component]
fn Foundation() -> Element {
    rsx! {
        main { class: "browse-page product-shell",
            header { class: "product-header",
                div { class: "product-kicker", "PM-38 foundation" }
                div { class: "product-header-row",
                    div {
                        h1 { "Foundation" }
                        p { class: "product-lede",
                            "Self-hosted typography, blueprint tokens, and reusable front-end states for upcoming product slices."
                        }
                    }
                    nav { class: "product-nav", aria_label: "Foundation navigation",
                        Link { to: Route::Browse {}, "Back to home" }
                        a { href: "#ui-states", "UI states" }
                    }
                }
            }

            section { id: "browse-foundation", class: "foundation-hero blueprint-panel",
                div { class: "foundation-hero-copy",
                    p { class: "eyebrow", "Token and asset check" }
                    h2 { "Blueprint styling foundation" }
                    p {
                        "This route keeps PM-38 primitives visible without making implementation scaffolding the product home."
                    }
                    div { class: "button-row",
                        button { class: "button button-primary", "Primary action" }
                        button { class: "button button-ghost", "Ghost action" }
                    }
                }
                div { class: "blueprint-preview", aria_label: "Blueprint placeholder preview",
                    div { class: "blueprint-preview-grid" }
                    span { class: "blueprint-axis blueprint-axis-x", "X" }
                    span { class: "blueprint-axis blueprint-axis-y", "Y" }
                    span { class: "blueprint-dimension", "120 × 86 × 42 mm" }
                }
            }

            section { class: "foundation-grid", aria_label: "Foundation primitives",
                for item in foundation_items() {
                    FoundationCard { item }
                }
            }

            section { id: "ui-states", class: "state-grid", aria_label: "Reusable states",
                div { class: "state-card loading-state",
                    span { class: "status-pill", "Loading" }
                    div { class: "skeleton-line skeleton-wide" }
                    div { class: "skeleton-line" }
                    div { class: "skeleton-box" }
                }
                div { class: "state-card empty-state",
                    span { class: "status-pill status-muted", "Empty" }
                    h2 { "No projects yet" }
                    p { "Empty browse states can use this panel before live feeds are connected." }
                }
                div { class: "state-card error-state",
                    span { class: "status-pill status-error", "Error" }
                    h2 { "Feed unavailable" }
                    p { "Error states keep the browse surface useful without inventing data plumbing in this slice." }
                }
            }
        }
    }
}

#[component]
fn Viewer(model_id: String) -> Element {
    let model = demo_model_for_route(&model_id);

    rsx! {
        main { class: "viewer-route-page",
            nav { class: "viewer-route-nav", aria_label: "Viewer navigation",
                Link { to: Route::Browse {}, "← Back to browse" }
            }
            ViewerPage { key: "{model.id}", initial_model: model }
        }
    }
}

#[derive(Clone, Copy, PartialEq)]
struct FoundationItem {
    label: &'static str,
    title: &'static str,
    body: &'static str,
    meta: &'static str,
}

fn foundation_items() -> [FoundationItem; 3] {
    [
        FoundationItem {
            label: "Panel",
            title: "Project card surface",
            body: "A crisp bordered card for future model summaries, previews, and maker metadata.",
            meta: "primitive/card",
        },
        FoundationItem {
            label: "Input",
            title: "Search and filter shell",
            body: "Reusable input styling for browse, profile, and publish flows without binding to a data source yet.",
            meta: "primitive/input",
        },
        FoundationItem {
            label: "Status",
            title: "Metadata and chips",
            body: "Compact mono labels for license, dimensions, file state, and feed status.",
            meta: "primitive/status",
        },
    ]
}

#[component]
fn FoundationCard(item: FoundationItem) -> Element {
    rsx! {
        article { class: "foundation-card card-surface",
            div { class: "card-media blueprint-media",
                span { class: "status-pill", "{item.label}" }
            }
            div { class: "card-body",
                h2 { class: "card-title", "{item.title}" }
                p { "{item.body}" }
                span { class: "card-meta", "{item.meta}" }
            }
        }
    }
}

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

    #[test]
    fn foundation_items_cover_base_primitives() {
        let items = foundation_items();
        assert!(items.iter().any(|item| item.label == "Panel"));
        assert!(items.iter().any(|item| item.label == "Input"));
        assert!(items.iter().any(|item| item.label == "Status"));
    }

    #[test]
    fn foundation_copy_avoids_workflow_marketing() {
        let combined = foundation_items()
            .iter()
            .map(|item| item.body)
            .collect::<Vec<_>>()
            .join(" ");
        assert!(!combined.contains("Jira"));
        assert!(!combined.contains("Polytoken"));
    }

    #[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:?}"),
        }
    }
}
