//! Same-origin Polymodel appview client for browser UI calls.
//!
//! Polymodel deliberately uses this thin client rather than Jacquard's
//! credential/session `BasicClient` path: appview reads should target this
//! server's `/xrpc/space.polymodel.*` routes and rely on browser-managed
//! cookies for OAuth sessions. Callers send generated payload
//! request values, for example `client.send(GetSession).await?.into_output()`.
//! Identity resolution is delegated to Jacquard's resolver over the same
//! injected HTTP transport; this keeps Polymodel's appview client thin without
//! adopting a credential/session agent for read calls.

use std::fmt;
use std::sync::{Arc, RwLock};

use jacquard::identity::JacquardResolver;
use jacquard::identity::resolver::{
    DidDocResponse, IdentityError, IdentityResolver, ResolverOptions,
};
use jacquard_common::BosStr;
use jacquard_common::http_client::HttpClient;
use jacquard_common::types::did::Did;
use jacquard_common::types::string::Handle;
use jacquard_common::xrpc::{CallOptions, XrpcClient, XrpcExt, XrpcRequest, XrpcResponse};
use jacquard_common::{deps::fluent_uri::Uri, error::XrpcResult};

const DEFAULT_NATIVE_BASE_URI: &str = "http://127.0.0.1:8080";

#[derive(Clone)]
pub struct PolymodelClient {
    http: reqwest::Client,
    resolver: JacquardResolver<reqwest::Client>,
    base_uri: Arc<RwLock<Uri<String>>>,
    opts: Arc<RwLock<CallOptions>>,
}

impl fmt::Debug for PolymodelClient {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PolymodelClient")
            .field("base_uri", &self.current_base_uri().as_str())
            .finish_non_exhaustive()
    }
}

impl PolymodelClient {
    pub fn new() -> Result<Self, ClientError> {
        Self::with_base_uri(default_base_uri()?)
    }

    pub fn with_base_uri(base_uri: Uri<String>) -> Result<Self, ClientError> {
        Self::with_http_and_base_uri(reqwest::Client::new(), base_uri)
    }

    pub fn with_http_and_base_uri(
        http: reqwest::Client,
        base_uri: Uri<String>,
    ) -> Result<Self, ClientError> {
        let resolver = JacquardResolver::new(http.clone(), ResolverOptions::default());
        Ok(Self {
            http,
            resolver,
            base_uri: Arc::new(RwLock::new(base_uri)),
            opts: Arc::new(RwLock::new(CallOptions::default())),
        })
    }

    pub fn parse_base_uri(value: &str) -> Result<Uri<String>, ClientError> {
        Uri::parse(value.to_string()).map_err(|_| ClientError::InvalidBaseUri(value.to_string()))
    }

    pub fn current_base_uri(&self) -> Uri<String> {
        self.base_uri
            .read()
            .expect("Polymodel client base URI lock is not poisoned")
            .clone()
    }
}

impl Default for PolymodelClient {
    fn default() -> Self {
        Self::new().expect("default Polymodel appview base URI is valid")
    }
}

impl HttpClient for PolymodelClient {
    type Error = reqwest::Error;

    async fn send_http(
        &self,
        request: http::Request<Vec<u8>>,
    ) -> Result<http::Response<Vec<u8>>, Self::Error> {
        self.http.send_http(request).await
    }
}

impl XrpcClient for PolymodelClient {
    async fn base_uri(&self) -> Uri<String> {
        self.current_base_uri()
    }

    async fn set_base_uri(&self, uri: Uri<String>) {
        *self
            .base_uri
            .write()
            .expect("Polymodel client base URI lock is not poisoned") = uri;
    }

    async fn opts(&self) -> CallOptions {
        self.opts
            .read()
            .expect("Polymodel client XRPC options lock is not poisoned")
            .clone()
    }

    async fn set_opts(&self, opts: CallOptions) {
        *self
            .opts
            .write()
            .expect("Polymodel client XRPC options lock is not poisoned") = opts;
    }

    #[cfg(not(target_arch = "wasm32"))]
    async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
    where
        R: XrpcRequest + Send + Sync + serde::Serialize,
        <R as XrpcRequest>::Response: Send + Sync,
        Self: Sync,
    {
        let base = self.base_uri().await;
        let opts = self.opts().await;
        let base_ref: Uri<&str> = base.borrow();
        self.xrpc(base_ref).with_options(opts).send(&request).await
    }

    #[cfg(target_arch = "wasm32")]
    async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
    where
        R: XrpcRequest + Send + Sync + serde::Serialize,
        <R as XrpcRequest>::Response: Send + Sync,
    {
        let base = self.base_uri().await;
        let opts = self.opts().await;
        let base_ref: Uri<&str> = base.borrow();
        self.xrpc(base_ref).with_options(opts).send(&request).await
    }

    #[cfg(not(target_arch = "wasm32"))]
    async fn send_with_opts<R>(&self, request: R, opts: CallOptions) -> XrpcResult<XrpcResponse<R>>
    where
        R: XrpcRequest + Send + Sync + serde::Serialize,
        <R as XrpcRequest>::Response: Send + Sync,
        Self: Sync,
    {
        let base = self.base_uri().await;
        let base_ref: Uri<&str> = base.borrow();
        self.xrpc(base_ref).with_options(opts).send(&request).await
    }

    #[cfg(target_arch = "wasm32")]
    async fn send_with_opts<R>(&self, request: R, opts: CallOptions) -> XrpcResult<XrpcResponse<R>>
    where
        R: XrpcRequest + Send + Sync + serde::Serialize,
        <R as XrpcRequest>::Response: Send + Sync,
    {
        let base = self.base_uri().await;
        let base_ref: Uri<&str> = base.borrow();
        self.xrpc(base_ref).with_options(opts).send(&request).await
    }
}

impl IdentityResolver for PolymodelClient {
    fn options(&self) -> &ResolverOptions {
        self.resolver.options()
    }

    #[cfg(not(target_arch = "wasm32"))]
    async fn resolve_handle<S: BosStr + Sync>(
        &self,
        handle: &Handle<S>,
    ) -> Result<Did, IdentityError>
    where
        Self: Sync,
    {
        self.resolver.resolve_handle(handle).await
    }

    #[cfg(target_arch = "wasm32")]
    async fn resolve_handle<S: BosStr + Sync>(
        &self,
        handle: &Handle<S>,
    ) -> Result<Did, IdentityError> {
        self.resolver.resolve_handle(handle).await
    }

    #[cfg(not(target_arch = "wasm32"))]
    async fn resolve_did_doc<S: BosStr + Sync>(
        &self,
        did: &Did<S>,
    ) -> Result<DidDocResponse, IdentityError>
    where
        Self: Sync,
    {
        self.resolver.resolve_did_doc(did).await
    }

    #[cfg(target_arch = "wasm32")]
    async fn resolve_did_doc<S: BosStr + Sync>(
        &self,
        did: &Did<S>,
    ) -> Result<DidDocResponse, IdentityError> {
        self.resolver.resolve_did_doc(did).await
    }
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ClientError {
    #[error("invalid Polymodel appview base URI: {0}")]
    InvalidBaseUri(String),
}

fn default_base_uri() -> Result<Uri<String>, ClientError> {
    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
    {
        let origin = web_sys::window()
            .and_then(|window| window.location().origin().ok())
            .filter(|origin| !origin.is_empty())
            .unwrap_or_else(|| DEFAULT_NATIVE_BASE_URI.to_string());
        PolymodelClient::parse_base_uri(&origin)
    }

    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
    {
        PolymodelClient::parse_base_uri(DEFAULT_NATIVE_BASE_URI)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use jacquard::ToSmolStr;
    use jacquard_common::xrpc::{CallOptions, build_http_request};
    use polymodel_api::space_polymodel::actor::get_session::GetSession;
    use polymodel_api::space_polymodel::library::get_feed::GetFeed;

    #[test]
    fn stores_base_uri_without_rewriting_it() {
        let client = PolymodelClient::with_base_uri(
            PolymodelClient::parse_base_uri("https://example.test/app").unwrap(),
        )
        .unwrap();

        assert_eq!(
            client.current_base_uri().as_str(),
            "https://example.test/app"
        );
    }

    #[test]
    fn get_feed_payload_builds_same_origin_xrpc_query() {
        let base = Uri::parse("https://example.test".to_string()).unwrap();
        let base_ref = Uri::parse(base.as_str()).unwrap();
        let request = GetFeed::new()
            .algorithm(Some("recent".to_smolstr()))
            .limit(24)
            .build();
        let http = build_http_request(&base_ref, &request, &CallOptions::default()).unwrap();

        assert_eq!(http.method(), http::Method::GET);
        assert_eq!(
            http.uri().to_string(),
            "https://example.test/xrpc/space.polymodel.library.getFeed?algorithm=recent&limit=24"
        );
    }

    #[test]
    fn get_session_payload_builds_no_param_query() {
        let base = Uri::parse("https://example.test".to_string()).unwrap();
        let base_ref = Uri::parse(base.as_str()).unwrap();
        let http = build_http_request(&base_ref, &GetSession, &CallOptions::default()).unwrap();

        assert_eq!(http.method(), http::Method::GET);
        assert_eq!(
            http.uri().to_string(),
            "https://example.test/xrpc/space.polymodel.actor.getSession"
        );
    }

    #[test]
    fn polymodel_client_exposes_jacquard_identity_resolver_options() {
        let client = PolymodelClient::with_base_uri(
            PolymodelClient::parse_base_uri("https://example.test").unwrap(),
        )
        .unwrap();

        let _: &ResolverOptions = client.options();
    }

    #[test]
    fn client_error_formats_for_ui() {
        let error = PolymodelClient::parse_base_uri("not a uri").unwrap_err();
        assert_eq!(
            error.to_string(),
            "invalid Polymodel appview base URI: not a uri"
        );
    }
}
