//! Server-side bridge for SSR and Dioxus server functions.
//!
//! Normal SSR/server-function reads should extract the inbound OAuth session
//! from the current Axum request and call appview helpers directly in-process.
//! The `x-jacquard-session` utility below is only an escape hatch for the rare
//! server-side outbound XRPC call that cannot be avoided.

use axum::extract::FromRequestParts;
use dioxus::prelude::{ServerFnError, dioxus_fullstack::FullstackContext};
use jacquard::identity::PublicResolver;
use jacquard::oauth::client::OAuthSession;
use jacquard_axum::oauth::ExtractOptionalOAuthSession;
use jacquard_common::types::ident::AtIdentifier;
use polymodel_api::space_polymodel::actor::ProfileViewUnion;
use polymodel_api::space_polymodel::actor::get_session::GetSessionOutput;
use polymodel_api::space_polymodel::library::FeedView;

use super::actor;
use super::error::clamp_limit;
use super::state::AppState;
use super::views::{self, ModelDetail};
use crate::oauth::SqliteAuthStore;

/// Maximum time to wait for the Bluesky profile fallback during SSR before
/// degrading to an error state. The polymodel-first path is pure SQLite and
/// needs no timeout; this only bounds the outbound `public.api.bsky.app` call.
const SSR_BSKY_PROFILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);

pub type PolymodelOAuthSession = OAuthSession<PublicResolver, SqliteAuthStore>;

/// Extract the current request's optional OAuth session and resolve the
/// `getSession` appview output without issuing a same-origin XRPC request.
pub async fn get_session_output_from_fullstack_context() -> Result<GetSessionOutput, ServerFnError>
{
    let (context, state) = context_and_state()?;
    let session = extract_optional_oauth_session(&context, &state).await?;
    actor::get_session_output(&state, session.as_ref())
        .await
        .map_err(|error| ServerFnError::new(format!("{error:?}")))
}

// ---------------------------------------------------------------------------
// Per-read "from fullstack context" helpers
//
// Each helper resolves the viewer from the request's OAuth session and calls
// the same in-process `views::`/`actor::` helper that the corresponding
// `/xrpc/*` handler calls. Parity is at the helper level: both paths share the
// same query/view logic.
// ---------------------------------------------------------------------------

/// Resolve a thing (with its models) server-side, same path as `getThing`.
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn get_thing_from_fullstack_context(
    uri: &str,
) -> Result<
    (
        polymodel_api::space_polymodel::library::ThingView,
        Vec<polymodel_api::space_polymodel::library::ModelView>,
    ),
    ServerFnError,
> {
    let (context, state) = context_and_state()?;
    let session = extract_optional_oauth_session(&context, &state).await?;
    let viewer = views::viewer_did(session.as_ref()).await;
    let viewer = viewer.as_ref().map(|d| d.borrow());
    views::get_thing(&state, uri, viewer)
        .await
        .map_err(|error| ServerFnError::new(format!("{error:?}")))
}

/// Resolve a model (with its parts and parent thing) server-side, same path as
/// `getModel`.
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn get_model_from_fullstack_context(
    uri: &str,
) -> Result<ModelDetail, ServerFnError> {
    let (context, state) = context_and_state()?;
    let session = extract_optional_oauth_session(&context, &state).await?;
    let viewer = views::viewer_did(session.as_ref()).await;
    let viewer = viewer.as_ref().map(|d| d.borrow());
    views::get_model(&state, uri, viewer)
        .await
        .map_err(|error| ServerFnError::new(format!("{error:?}")))
}

/// Resolve a feed server-side, same path as `getFeed`. The `algorithm` string
/// dispatches exactly as the XRPC handler.
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn get_feed_from_fullstack_context(
    algorithm: &str,
    limit: i64,
) -> Result<FeedView, ServerFnError> {
    let (context, state) = context_and_state()?;
    let session = extract_optional_oauth_session(&context, &state).await?;
    let viewer = views::viewer_did(session.as_ref()).await;
    let viewer = viewer.as_ref().map(|d| d.borrow());
    let limit = clamp_limit(Some(limit));
    let feed = match algorithm {
        "recent" => views::feed_recent(&state, limit, None, viewer).await,
        "hot" => views::feed_hot(&state, limit, None, viewer).await,
        "following" => match viewer {
            Some(viewer) => views::feed_following(&state, viewer, limit, None).await,
            None => Ok(views::empty_feed()),
        },
        other => return Err(ServerFnError::new(format!("unknown algorithm: {other}"))),
    };
    feed.map_err(|error| ServerFnError::new(format!("{error:?}")))
}

/// Resolve a profile union server-side, same path as `getProfile`. Polymodel
/// profile is tried first (pure SQLite); the Bluesky fallback is bounded by
/// [`SSR_BSKY_PROFILE_TIMEOUT`] so a slow upstream cannot block first paint.
/// On timeout or error the caller receives an `Err`, which the route maps to
/// its existing error state.
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn get_profile_union_from_fullstack_context(
    actor_ident: &AtIdentifier,
) -> Result<ProfileViewUnion, ServerFnError> {
    let (_context, state) = context_and_state()?;
    let did = actor::resolve_actor(&state, actor_ident)
        .await
        .map_err(|error| ServerFnError::new(format!("{error:?}")))?;

    if let Some(profile) = views::polymodel_profile_view(&state, &did)
        .await
        .map_err(|error| ServerFnError::new(format!("{error:?}")))?
    {
        return Ok(ProfileViewUnion::ProfileView(Box::new(profile)));
    }

    match tokio::time::timeout(
        SSR_BSKY_PROFILE_TIMEOUT,
        actor::fetch_bsky_profile(&state, actor_ident),
    )
    .await
    {
        Ok(Ok(detailed)) => Ok(ProfileViewUnion::ProfileViewDetailed(Box::new(detailed))),
        Ok(Err(error)) => Err(ServerFnError::new(format!(
            "Profile request failed: {error:?}"
        ))),
        Err(_) => Err(ServerFnError::new(
            "Profile request timed out waiting for Bluesky fallback",
        )),
    }
}

/// Resolve an actor's things server-side, same path as `getAuthorThings`. The
/// actor identifier is canonicalized to a DID before querying, exactly as the
/// XRPC handler does.
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn get_author_things_from_fullstack_context(
    actor_ident: &AtIdentifier,
    limit: i64,
    cursor: Option<&str>,
) -> Result<FeedView, ServerFnError> {
    let (context, state) = context_and_state()?;
    let session = extract_optional_oauth_session(&context, &state).await?;
    let viewer = views::viewer_did(session.as_ref()).await;
    let viewer = viewer.as_ref().map(|d| d.borrow());
    let did = actor::resolve_actor(&state, actor_ident)
        .await
        .map_err(|error| ServerFnError::new(format!("{error:?}")))?;
    views::author_things(
        &state,
        did.as_ref(),
        clamp_limit(Some(limit)),
        cursor,
        viewer,
    )
    .await
    .map_err(|error| ServerFnError::new(format!("{error:?}")))
}

/// Search things server-side, same path as `searchThings`.
#[cfg_attr(not(feature = "server"), allow(dead_code))]
pub(crate) async fn get_search_from_fullstack_context(
    query: &str,
    cursor: Option<&str>,
    limit: i64,
) -> Result<FeedView, ServerFnError> {
    let (context, state) = context_and_state()?;
    let session = extract_optional_oauth_session(&context, &state).await?;
    let viewer = views::viewer_did(session.as_ref()).await;
    let viewer = viewer.as_ref().map(|d| d.borrow());
    views::search_things(&state, query, clamp_limit(Some(limit)), cursor, viewer)
        .await
        .map_err(|error| ServerFnError::new(format!("{error:?}")))
}

// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------

fn context_and_state() -> Result<(FullstackContext, AppState), ServerFnError> {
    let context = FullstackContext::current()
        .ok_or_else(|| ServerFnError::new("missing Dioxus fullstack request context"))?;
    let state = context
        .extension::<AppState>()
        .ok_or_else(|| ServerFnError::new("missing Polymodel AppState request extension"))?;
    Ok((context, state))
}

pub(crate) async fn extract_optional_oauth_session(
    context: &FullstackContext,
    state: &AppState,
) -> Result<Option<PolymodelOAuthSession>, ServerFnError> {
    let mut parts = context.parts_mut().clone();
    ExtractOptionalOAuthSession::<PublicResolver, SqliteAuthStore>::from_request_parts(
        &mut parts, state,
    )
    .await
    .map(|ExtractOptionalOAuthSession(session)| session)
    .map_err(|error| ServerFnError::new(error.to_string()))
}
