//! `space.polymodel.actor.*` read endpoint handlers: `getProfile` (typed union
//! of polymodel profileView / app.bsky profileViewDetailed) and `getSession`
//! (PM-9 identity primitive; PM-26 returns the unauthenticated marker).

use std::sync::Arc;

use axum::extract::State;
use jacquard::identity::resolver::IdentityResolver;
use jacquard_axum::{ExtractXrpc, XrpcResponse};
use jacquard_common::types::ident::AtIdentifier;
use jacquard_common::types::string::Did;
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::{
    ProfileViewUnion,
    get_profile::{GetProfileOutput, GetProfileRequest},
    get_session::{GetSessionOutput, GetSessionRequest, SessionUnauthenticated, SessionView},
};

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

/// 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<Arc<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 PM-26's
/// read endpoints; tests exercise union-variant construction without it.
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<Arc<AppState>>,
) -> AppResult<XrpcResponse<GetSessionRequest>> {
    // PM-26 ships the unauthenticated arm only. PM-27 will return
    // `SessionAuthenticated` once OAuth session extraction and profile writes are wired.

    Ok(XrpcResponse(GetSessionOutput {
        value: SessionView::SessionUnauthenticated(Box::new(SessionUnauthenticated {
            authenticated: false,
            extra_data: None,
        })),
        extra_data: None,
    }))
}
