//! XRPC read endpoints (appview query layer).
//!
//! All read endpoints are backed by the SQLite projection — records in
//! `record_json`, handles in `identities`, profiles in `profiles` — so the read
//! path needs no Hydrant handle. Endpoints mount via `jacquard-axum`'s
//! [`IntoRouter`] and share [`AppState`] (SQLite pool + identity resolver + bsky
//! client).
//!
//! Not yet implemented: the SSR bridge (PM-46) and the PDS passthrough proxy
//! (PM-47).

pub mod error;
pub mod state;
pub mod views;

mod actor;
mod graph;
mod library;
mod writes;

#[cfg(test)]
mod tests;

use axum::Router;
use jacquard_axum::IntoRouter;
use jacquard_common::xrpc::XrpcEndpoint;
use polymodel_api::space_polymodel::{
    actor::{
        bootstrap_profile::BootstrapProfileRequest, get_profile::GetProfileRequest,
        get_session::GetSessionRequest,
    },
    graph::{
        create_like::CreateLikeRequest, create_save::CreateSaveRequest,
        create_tag::CreateTagRequest, delete_like::DeleteLikeRequest,
        delete_save::DeleteSaveRequest, delete_tag::DeleteTagRequest, get_list::GetListRequest,
    },
    library::{
        delete_thing::DeleteThingRequest, get_author_things::GetAuthorThingsRequest,
        get_feed::GetFeedRequest, get_model::GetModelRequest, get_thing::GetThingRequest,
        publish_thing::PublishThingRequest, search_things::SearchThingsRequest,
        stage_file::StageFileRequest, update_thing::UpdateThingRequest,
    },
};

use self::state::AppState;

/// Build the appview XRPC router with the given state applied.
///
/// Returns a state-bound `Router<AppState>`; merged with the OAuth routes and the Dioxus SSR router in `main`, where `.with_state` is applied once.
pub fn router() -> Router<AppState> {
    Router::new()
        .merge(GetThingRequest::into_router(library::get_thing))
        .merge(GetModelRequest::into_router(library::get_model))
        .merge(GetAuthorThingsRequest::into_router(
            library::get_author_things,
        ))
        .merge(GetFeedRequest::into_router(library::get_feed))
        .merge(SearchThingsRequest::into_router(library::search_things))
        .merge(GetListRequest::into_router(graph::get_list))
        .merge(GetProfileRequest::into_router(actor::get_profile))
        .merge(BootstrapProfileRequest::into_router(
            writes::bootstrap_profile,
        ))
        .route(
            StageFileRequest::PATH,
            axum::routing::post(writes::stage_file),
        )
        .merge(PublishThingRequest::into_router(writes::publish_thing))
        .merge(UpdateThingRequest::into_router(writes::update_thing))
        .merge(DeleteThingRequest::into_router(writes::delete_thing))
        .merge(CreateLikeRequest::into_router(writes::create_like))
        .merge(DeleteLikeRequest::into_router(writes::delete_like))
        .merge(CreateSaveRequest::into_router(writes::create_save))
        .merge(DeleteSaveRequest::into_router(writes::delete_save))
        .merge(CreateTagRequest::into_router(writes::create_tag))
        .merge(DeleteTagRequest::into_router(writes::delete_tag))
        // getSession takes no parameters, so it bypasses ExtractXrpc (which
        // decodes the query string; a unit-struct request from an empty query is
        // rejected by serde_html_form). Identity comes from the session.
        .route(
            GetSessionRequest::PATH,
            axum::routing::get(actor::get_session),
        )
        // Per-request timing: logs total server-side handling time for every
        // appview request, *including* the OAuth session-restore extractor that
        // runs before each handler body. Use this to localize latency (server
        // handler vs. extractor vs. a client firing the request late).
        .layer(axum::middleware::from_fn(time_request))
}

async fn time_request(
    req: axum::extract::Request,
    next: axum::middleware::Next,
) -> axum::response::Response {
    let method = req.method().clone();
    let path = req.uri().path().to_string();
    let start = std::time::Instant::now();
    let response = next.run(req).await;
    let elapsed_ms = start.elapsed().as_millis();
    let status = response.status().as_u16();
    if elapsed_ms >= 500 {
        tracing::warn!(%method, path, status, elapsed_ms, "slow appview request");
    } else {
        tracing::info!(%method, path, status, elapsed_ms, "appview request");
    }
    response
}
