//! 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).
//!
//! In addition to the `space.polymodel.*` endpoints below, this module mounts a
//! fixed, structural allowlist of non-polymodel XRPC methods as a single-origin
//! passthrough proxy (PM-47): the direct `com.atproto.repo.*` record operations
//! plus `com.atproto.identity.resolveHandle` and `app.bsky.actor.getProfile`,
//! dispatched over their generated typed structs to the user's PDS / public
//! AppView. Every other NSID has no route and 404s, and upstream XRPC errors are
//! forwarded with their real status + body.

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

mod actor;
mod downloads;
mod drafts;
mod graph;
mod library;
mod proxy;
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_follow::CreateFollowRequest, create_like::CreateLikeRequest,
        create_save::CreateSaveRequest, create_tag::CreateTagRequest,
        delete_follow::DeleteFollowRequest, 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 polymodel_api::app_bsky::actor::get_profile::GetProfileRequest as AppBskyGetProfileRequest;
use polymodel_api::com_atproto::identity::resolve_handle::ResolveHandleRequest;
use polymodel_api::com_atproto::repo::{
    create_record::CreateRecordRequest, delete_record::DeleteRecordRequest,
    describe_repo::DescribeRepoRequest, get_record::GetRecordRequest,
    list_records::ListRecordsRequest, put_record::PutRecordRequest, upload_blob::UploadBlobRequest,
};

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(CreateFollowRequest::into_router(writes::create_follow))
        .merge(DeleteFollowRequest::into_router(writes::delete_follow))
        .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))
        // PM-47 passthrough proxy: a structural allowlist of non-polymodel XRPC
        // methods forwarded to the user's PDS / public AppView over their
        // generated typed request structs. Any other NSID has no route → 404.
        .merge(GetRecordRequest::into_router(proxy::repo_get_record))
        .merge(ListRecordsRequest::into_router(proxy::repo_list_records))
        .merge(DescribeRepoRequest::into_router(proxy::repo_describe_repo))
        .merge(CreateRecordRequest::into_router(proxy::repo_create_record))
        .merge(PutRecordRequest::into_router(proxy::repo_put_record))
        .merge(DeleteRecordRequest::into_router(proxy::repo_delete_record))
        .merge(UploadBlobRequest::into_router(proxy::repo_upload_blob))
        .merge(ResolveHandleRequest::into_router(
            proxy::identity_resolve_handle,
        ))
        .merge(AppBskyGetProfileRequest::into_router(
            proxy::actor_get_profile,
        ))
        // 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). It uses Jacquard's API/headless optional
        // extractor so the documented x-jacquard-session fallback is confined to
        // this session bridge surface; other public read endpoints keep the
        // browser-oriented optional extractor semantics.
        .route(
            GetSessionRequest::PATH,
            axum::routing::get(actor::get_session),
        )
        .route(
            "/app/parts/{did}/{rkey}/download",
            axum::routing::get(downloads::download_part),
        )
        // PM-43 app-internal draft store + image upload. Not federated lexicons,
        // so these use a plain `/app/*` namespace (cookie-authenticated,
        // same-origin) rather than `/xrpc/space.polymodel.*`.
        .route(
            "/app/drafts",
            axum::routing::post(drafts::create_draft).get(drafts::list_drafts_handler),
        )
        .route(
            "/app/drafts/{draft_id}",
            axum::routing::put(drafts::put_draft)
                .get(drafts::get_draft_handler)
                .delete(drafts::delete_draft_handler),
        )
        .route(
            "/app/drafts/{draft_id}/publish",
            axum::routing::post(drafts::publish_draft),
        )
        .route("/app/images", axum::routing::post(drafts::upload_image))
        // 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
}
