//! `space.polymodel.actor.*` read endpoint handlers: `getProfile` (typed union
//! of polymodel profileView / app.bsky profileViewDetailed) and `getSession`
//! (the session identity primitive).

use axum::extract::State;
use jacquard::identity::PublicResolver;
use jacquard::identity::resolver::IdentityResolver;
use jacquard::oauth::client::OAuthSession;
use jacquard_axum::oauth::ExtractOptionalOAuthSession;
use jacquard_axum::{ExtractXrpc, XrpcResponse};
use jacquard_common::deps::smol_str::SmolStr;
use jacquard_common::types::ident::AtIdentifier;
use jacquard_common::types::string::{Did, Handle};
use jacquard_common::types::value::Data;
use jacquard_common::xrpc::XrpcClient;
use polymodel_api::app_bsky::actor::ProfileViewDetailed;
use polymodel_api::app_bsky::actor::get_profile::GetProfile as BskyGetProfile;
use polymodel_api::space_polymodel::actor::{
    ProfileView, ProfileViewUnion,
    get_profile::{GetProfileOutput, GetProfileRequest},
    get_session::{
        GetSessionOutput, GetSessionRequest, SessionAuthenticated, SessionUnauthenticated,
        SessionView,
    },
};

use super::error::{AppResult, internal};
use super::state::AppState;
use super::views;
use crate::oauth::SqliteAuthStore;

/// Canonicalize an at-identifier to a DID by matching on its variant (not by
/// sniffing its serialized form). DIDs pass through; handles are resolved via
/// the identity resolver. A handle that cannot be resolved is a 500 (the
/// identifier was already structurally valid).
pub(super) async fn resolve_actor(state: &AppState, ident: &AtIdentifier) -> AppResult<Did> {
    match ident {
        AtIdentifier::Did(did) => Ok(did.clone()),
        AtIdentifier::Handle(handle) => state
            .resolver
            .resolve_handle(handle)
            .await
            .map_err(|e| internal(format!("handle resolution failed: {e}"))),
    }
}

pub(super) async fn get_profile(
    State(state): State<AppState>,
    ExtractXrpc(req): ExtractXrpc<GetProfileRequest>,
) -> AppResult<XrpcResponse<GetProfileRequest>> {
    let did = resolve_actor(&state, &req.actor).await?;

    // Polymodel variant when the actor authored a polymodel profile record.
    if let Some(profile) = views::polymodel_profile_view(&state, &did).await? {
        return Ok(XrpcResponse(GetProfileOutput {
            value: ProfileViewUnion::ProfileView(Box::new(profile)),
            extra_data: None,
        }));
    }

    // Otherwise the bluesky variant, fetched from the public AppView.
    let detailed = fetch_bsky_profile(&state, &req.actor).await?;
    Ok(XrpcResponse(GetProfileOutput {
        value: ProfileViewUnion::ProfileViewDetailed(Box::new(detailed)),
        extra_data: None,
    }))
}

/// Fetch `app.bsky.actor.getProfile` from `https://public.api.bsky.app` via the
/// unauthenticated jacquard client. This is the only network path in the read
/// endpoints; tests exercise union-variant construction without it.
pub(crate) async fn fetch_bsky_profile(
    state: &AppState,
    ident: &AtIdentifier,
) -> AppResult<ProfileViewDetailed> {
    let resp = state
        .bsky
        .send(BskyGetProfile {
            actor: ident.clone(),
        })
        .await
        .map_err(|e| internal(format!("app.bsky.actor.getProfile failed: {e}")))?;
    let out = resp
        .into_output()
        .map_err(|e| internal(format!("app.bsky.actor.getProfile decode failed: {e}")))?;
    Ok(out.value)
}

pub(super) async fn get_session(
    State(state): State<AppState>,
    ExtractOptionalOAuthSession(session): ExtractOptionalOAuthSession<
        PublicResolver,
        SqliteAuthStore,
    >,
) -> AppResult<XrpcResponse<GetSessionRequest>> {
    Ok(XrpcResponse(
        get_session_output(&state, session.as_ref()).await?,
    ))
}

pub(crate) async fn get_session_output(
    state: &AppState,
    session: Option<&OAuthSession<PublicResolver, SqliteAuthStore>>,
) -> AppResult<GetSessionOutput> {
    if let Some(session) = session.as_ref() {
        let t_session = std::time::Instant::now();
        let did = session.session_info().await.0;
        let session_info_ms = t_session.elapsed().as_millis();
        let t_resolve = std::time::Instant::now();
        let resolved = views::resolve_handle(state, &did).await;
        let resolve_handle_ms = t_resolve.elapsed().as_millis();
        match resolved {
            Ok(handle_str) => {
                let handle = Handle::new_owned(handle_str.clone())
                    .map_err(|e| internal(format!("invalid session handle: {e}")))?;
                let t_profile = std::time::Instant::now();
                let profile = match views::polymodel_profile_view(state, &did).await? {
                    Some(profile) => profile,
                    None => {
                        // No polymodel profile record: fall back to the actor's
                        // Bluesky profile so the session carries their real avatar
                        // and display name. If that fetch fails, degrade to a
                        // handle-only profile rather than dropping the session.
                        match fetch_bsky_profile(state, &AtIdentifier::Did(did.clone())).await {
                            Ok(detailed) => {
                                bsky_session_profile(detailed, did.clone(), &handle_str)
                            }
                            Err(error) => {
                                tracing::warn!(error = ?error, did = %did, "bsky profile fetch failed for session; using handle-only profile");
                                minimal_session_profile(did.clone(), handle_str)?
                            }
                        }
                    }
                };
                tracing::info!(
                    session_info_ms,
                    resolve_handle_ms,
                    profile_ms = t_profile.elapsed().as_millis(),
                    "get_session authed timing"
                );
                return Ok(GetSessionOutput {
                    value: SessionView::SessionAuthenticated(Box::new(SessionAuthenticated {
                        authenticated: true,
                        did,
                        handle,
                        profile,
                        extra_data: None,
                    })),
                    extra_data: None,
                });
            }
            Err(error) => {
                tracing::warn!(error = ?error, did = %did, "session handle resolution failed; reporting unauthenticated");
            }
        }
    }

    Ok(unauthenticated_session())
}

fn unauthenticated_session() -> GetSessionOutput {
    GetSessionOutput {
        value: SessionView::SessionUnauthenticated(Box::new(SessionUnauthenticated {
            authenticated: false,
            extra_data: None,
        })),
        extra_data: None,
    }
}

fn minimal_session_profile(did: Did, handle: String) -> AppResult<ProfileView> {
    Ok(ProfileView {
        avatar: None,
        default_license: None,
        description: None,
        did,
        display_name: None,
        handle: SmolStr::from(handle),
        indexed_at: None,
        links: None,
        printers: None,
        pronouns: None,
        record: Data::Null,
        follower_count: None,
        following_count: None,
        lists: None,
        thing_count: None,
        total_likes: None,
        viewer: None,
        extra_data: None,
    })
}

/// Build a session `ProfileView` from the actor's Bluesky profile so the
/// signed-in chrome can show their real avatar and display name when they have
/// no polymodel profile record. Polymodel-specific fields stay absent.
fn bsky_session_profile(detailed: ProfileViewDetailed, did: Did, handle: &str) -> ProfileView {
    ProfileView {
        avatar: detailed.avatar,
        default_license: None,
        description: detailed.description,
        did,
        display_name: detailed.display_name,
        handle: SmolStr::from(handle),
        indexed_at: detailed.indexed_at,
        links: None,
        printers: None,
        pronouns: detailed.pronouns,
        record: Data::Null,
        follower_count: None,
        following_count: None,
        lists: None,
        thing_count: None,
        total_likes: None,
        viewer: None,
        extra_data: None,
    }
}
