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

use crate::Route;
use crate::profile::profile_href;
use crate::thing_card::ThingCard;
use crate::thing_detail::thing_detail_href;

const SEARCH_LIMIT: i64 = 24;

#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct SearchPageResult {
    query: String,
    items: Vec<library::ThingViewBasic>,
    cursor: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum LoadMoreState {
    Idle,
    Loading,
    Error(String),
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct SearchResults {
    query: String,
    items: Vec<library::ThingViewBasic>,
    cursor: Option<String>,
    load_more: LoadMoreState,
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum SearchState {
    Idle,
    Loading,
    Error(String),
    Empty,
    Populated(SearchResults),
}

fn search_state(
    query: &str,
    pending: bool,
    result: Option<&Result<SearchPageResult, String>>,
    current: Option<&SearchResults>,
) -> SearchState {
    let query = query.trim();
    if query.is_empty() {
        return SearchState::Idle;
    }

    if let Some(results) = current.filter(|results| results.query == query) {
        return SearchState::Populated(results.clone());
    }

    if pending && result.is_none() {
        return SearchState::Loading;
    }

    match result {
        Some(Ok(page)) if page.query != query => SearchState::Loading,
        Some(Ok(page)) if page.items.is_empty() => SearchState::Empty,
        Some(Ok(page)) => SearchState::Populated(SearchResults {
            query: page.query.clone(),
            items: page.items.clone(),
            cursor: page.cursor.clone(),
            load_more: LoadMoreState::Idle,
        }),
        Some(Err(error)) => SearchState::Error(error.clone()),
        None => SearchState::Loading,
    }
}

fn apply_load_more_result(
    state: &mut SearchResults,
    requested_query: &str,
    page: Result<SearchPageResult, String>,
) {
    if state.query != requested_query {
        return;
    }

    match page {
        Ok(page) if page.query == state.query => {
            state.items.extend(page.items);
            state.cursor = page.cursor;
            state.load_more = LoadMoreState::Idle;
        }
        Ok(_) => {}
        Err(error) => state.load_more = LoadMoreState::Error(error),
    }
}

#[allow(clippy::useless_format)]
#[component]
pub(crate) fn Search(q: String) -> Element {
    let route = use_route::<Route>();
    let route_query = match route {
        Route::Search { q } => q.trim().to_string(),
        _ => q.trim().to_string(),
    };
    let mut active_query = use_signal(|| route_query.clone());
    let mut results = use_signal(|| None::<SearchResults>);

    if *active_query.read() != route_query {
        active_query.set(route_query.clone());
        results.set(None);
    }

    let mut search = use_server_future(move || {
        let query = active_query.read().clone();
        search_seed(query, None, SEARCH_LIMIT)
    })?;

    let search_result = search.read();
    let mapped = search_result.as_ref().map(|r| match r {
        Ok(page) => Ok(page.clone()),
        Err(ServerFnError::ServerError { message, .. }) => Err(message.clone()),
        Err(other) => Err(other.to_string()),
    });
    let state = search_state(
        &route_query,
        search.pending(),
        mapped.as_ref(),
        results.read().as_ref(),
    );

    rsx! {
        main { class: "search-page",
            section { class: "search-intro blueprint-panel", aria_label: "Search",
                span { class: "status-pill status-muted", "Catalog search" }
                if !route_query.is_empty() {
                    p { class: "search-query", "\"{route_query}\"" }
                }
            }

            div { class: "search-content",
                match state {
                    SearchState::Idle => rsx! {
                        section { class: "state-card empty-state search-state-card", aria_label: "Search instructions",
                            span { class: "status-pill status-muted", "Ready" }
                            h2 { "Search from the header" }
                            p { "Find indexed projects by name, tag, author, or description." }
                            Link { class: "button button-secondary", to: Route::Browse {}, "Browse instead" }
                        }
                    },
                    SearchState::Loading => rsx! {
                        section { class: "search-results-grid", aria_label: "Search results loading",
                            for index in 0..6 {
                                div { key: "search-skeleton-{index}", class: "state-card loading-state",
                                    span { class: "status-pill", "Searching" }
                                    div { class: "skeleton-line skeleton-wide" }
                                    div { class: "skeleton-line" }
                                    div { class: "skeleton-box" }
                                }
                            }
                        }
                    },
                    SearchState::Error(error) => rsx! {
                        section { class: "state-card error-state search-state-card", aria_label: "Search error",
                            h2 { "Search failed" }
                            p { "{error}" }
                            button { class: "button button-secondary", onclick: move |_| search.restart(), "Retry" }
                        }
                    },
                    SearchState::Empty => rsx! {
                        section { class: "state-card empty-state search-state-card", aria_label: "No search results",
                            span { class: "status-pill status-muted", "No matches" }
                            h2 { "No projects match \"{route_query}\"" }
                            p { "Try a broader term, material, or category." }
                            Link { class: "button button-secondary", to: Route::Browse {}, "Back to browse" }
                        }
                    },
                    SearchState::Populated(search_results) => rsx! {
                        section { class: "search-results-grid", aria_label: "Search results",
                            for thing in search_results.items.clone() {
                                SearchThingCard { key: "{thing.uri}", thing }
                            }
                        }
                        {search_pagination(
                            route_query.clone(),
                            results,
                            search_results.clone(),
                            search_results.cursor.clone(),
                            search_results.load_more.clone(),
                        )}
                    },
                }
            }
        }
    }
}

#[server]
async fn search_seed(
    query: String,
    cursor: Option<String>,
    limit: i64,
) -> Result<SearchPageResult, ServerFnError> {
    let query = query.trim().to_string();
    if query.is_empty() {
        return Ok(SearchPageResult {
            query,
            items: Vec::new(),
            cursor: None,
        });
    }

    let feed =
        crate::appview::ssr::get_search_from_fullstack_context(&query, cursor.as_deref(), limit)
            .await?;

    Ok(SearchPageResult {
        query,
        items: feed.items.into_iter().map(|item| item.thing).collect(),
        cursor: feed.cursor.map(|c| c.to_string()),
    })
}

#[component]
fn SearchThingCard(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 } }
}

fn search_pagination(
    query: String,
    results: Signal<Option<SearchResults>>,
    current_results: SearchResults,
    cursor: Option<String>,
    load_more: LoadMoreState,
) -> Element {
    match (cursor, load_more) {
        (Some(cursor), LoadMoreState::Idle) => rsx! {
            div { class: "search-pagination", aria_label: "Search pagination",
                button {
                    class: "button button-secondary",
                    onclick: move |_| {
                        let query = query.clone();
                        let cursor = cursor.clone();
                        let current_results = current_results.clone();
                        let mut results = results;
                        results.set(Some(SearchResults {
                            load_more: LoadMoreState::Loading,
                            ..current_results
                        }));
                        spawn(async move {
                            let page = search_seed(query.clone(), Some(cursor), SEARCH_LIMIT)
                                .await
                                .map_err(|e| match e {
                                    ServerFnError::ServerError { message, .. } => message,
                                    other => other.to_string(),
                                });
                            results.with_mut(|state| {
                                let Some(state) = state else { return; };
                                apply_load_more_result(state, &query, page);
                            });
                        });
                    },
                    "Load more"
                }
            }
        },
        (Some(_), LoadMoreState::Loading) => rsx! {
            div { class: "search-pagination search-pagination-loading", aria_live: "polite",
                span { class: "status-pill", "Loading more" }
                div { class: "skeleton-line skeleton-wide" }
            }
        },
        (Some(cursor), LoadMoreState::Error(error)) => rsx! {
            div { class: "state-card error-state search-load-more-error", aria_label: "Load more failed",
                h2 { "Could not load more results" }
                p { "{error}" }
                button {
                    class: "button button-secondary",
                    onclick: move |_| {
                        let query = query.clone();
                        let cursor = cursor.clone();
                        let current_results = current_results.clone();
                        let mut results = results;
                        results.set(Some(SearchResults {
                            load_more: LoadMoreState::Loading,
                            ..current_results
                        }));
                        spawn(async move {
                            let page = search_seed(query.clone(), Some(cursor), SEARCH_LIMIT)
                                .await
                                .map_err(|e| match e {
                                    ServerFnError::ServerError { message, .. } => message,
                                    other => other.to_string(),
                                });
                            results.with_mut(|state| {
                                let Some(state) = state else { return; };
                                apply_load_more_result(state, &query, page);
                            });
                        });
                    },
                    "Retry load more"
                }
            }
        },
        (None, _) => rsx! {},
    }
}

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

    fn page(
        query: &str,
        items: Vec<library::ThingViewBasic>,
        cursor: Option<&str>,
    ) -> SearchPageResult {
        SearchPageResult {
            query: query.to_string(),
            items,
            cursor: cursor.map(str::to_string),
        }
    }

    #[test]
    fn search_state_distinguishes_idle_loading_empty_error_and_populated() {
        assert_eq!(search_state(" ", false, None, None), SearchState::Idle);
        assert_eq!(search_state("cube", true, None, None), SearchState::Loading);
        assert_eq!(
            search_state("cube", false, Some(&Ok(page("cube", vec![], None))), None),
            SearchState::Empty
        );
        assert_eq!(
            search_state(
                "cube",
                false,
                Some(&Err("index unavailable".to_string())),
                None,
            ),
            SearchState::Error("index unavailable".to_string())
        );

        match search_state(
            "cube",
            false,
            Some(&Ok(page("cube", thing_card_fixtures(), Some("search:24")))),
            None,
        ) {
            SearchState::Populated(results) => {
                assert_eq!(results.items.len(), 3);
                assert_eq!(results.cursor.as_deref(), Some("search:24"));
                assert_eq!(results.load_more, LoadMoreState::Idle);
            }
            state => panic!("expected populated state, got {state:?}"),
        }
    }

    #[test]
    fn search_state_preserves_loaded_results_during_pagination_states() {
        let base = SearchResults {
            query: "cube".into(),
            items: thing_card_fixtures(),
            cursor: Some("search:24".into()),
            load_more: LoadMoreState::Loading,
        };
        assert_eq!(
            search_state("cube", true, None, Some(&base)),
            SearchState::Populated(base.clone())
        );

        let errored = SearchResults {
            load_more: LoadMoreState::Error("next page failed".into()),
            ..base.clone()
        };
        assert_eq!(
            search_state("cube", false, None, Some(&errored)),
            SearchState::Populated(errored.clone())
        );

        let finished = SearchResults {
            cursor: None,
            load_more: LoadMoreState::Idle,
            ..base
        };
        assert_eq!(
            search_state("cube", false, None, Some(&finished)),
            SearchState::Populated(finished)
        );
    }

    #[test]
    fn search_state_ignores_results_from_previous_query() {
        let current = SearchResults {
            query: "cube".into(),
            items: thing_card_fixtures(),
            cursor: None,
            load_more: LoadMoreState::Idle,
        };
        assert_eq!(
            search_state("enclosure", true, None, Some(&current)),
            SearchState::Loading
        );
        assert_eq!(
            search_state(
                "enclosure",
                false,
                Some(&Ok(page("cube", thing_card_fixtures(), None))),
                None,
            ),
            SearchState::Loading
        );
    }

    #[test]
    fn load_more_completion_is_bound_to_requested_query() {
        let mut state = SearchResults {
            query: "enclosure".into(),
            items: thing_card_fixtures(),
            cursor: Some("search:24".into()),
            load_more: LoadMoreState::Loading,
        };

        apply_load_more_result(&mut state, "cube", Err("old request failed".into()));
        assert_eq!(state.load_more, LoadMoreState::Loading);
        assert_eq!(state.items.len(), 3);

        apply_load_more_result(
            &mut state,
            "cube",
            Ok(page("cube", thing_card_fixtures(), None)),
        );
        assert_eq!(state.load_more, LoadMoreState::Loading);
        assert_eq!(state.items.len(), 3);

        apply_load_more_result(
            &mut state,
            "enclosure",
            Ok(page("enclosure", thing_card_fixtures(), None)),
        );
        assert_eq!(state.load_more, LoadMoreState::Idle);
        assert_eq!(state.items.len(), 6);
        assert!(state.cursor.is_none());
    }
}
