// Copyright 2025 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// or the MIT license (http://opensource.org/licenses/MIT),
// at your option.

// Unless required by applicable law or agreed to in writing,
// this software is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
// specific language governing permissions and limitations under
// each license.

//! HTTP abstraction layer.
//!
//! This module defines generic traits and helpers for performing HTTP requests
//! without hard-wiring a specific HTTP client. It allows host applications to
//! plug in their own HTTP implementation, restrict where the SDK may connect,
//! or disable networking entirely.
//!
//! # When do network requests occur?
//!
//! The SDK may issue HTTP/S requests in the following scenarios:
//! - [`Reader`]:
//!     - Fetching remote manifests
//!     - Validating CAWG identity assertions
//!     - Fetching OCSP revocation status
//! - [`Builder`]:
//!     - Fetching ingredient remote manifests
//!     - Fetching timestamps
//!     - Fetching [`TimeStamp`] assertions
//!     - Fetching OCSP staples
//!
//! Network requests may also be issued during the signing process, such as when
//! [`SignerSettings::Remote`] is specified.
//!
//! [`Reader`]: crate::Reader
//! [`Builder`]: crate::Builder
//! [`TimeStamp`]: crate::assertions::TimeStamp
//! [`SignerSettings::Remote`]: crate::settings::signer::SignerSettings::Remote

use std::{
    io::{self, Cursor, Read},
    sync::Arc,
};

use async_trait::async_trait;
use http::{Request, Response};

use crate::{
    maybe_send_sync::{MaybeSend, MaybeSync},
    Result,
};

mod reqwest;
mod ureq;
mod wasi;

pub mod restricted;

// Since we expose `http::Request` and `http::Response` in the public API, we also expose the `http` crate.
pub use http;

/// A resolver for sync (blocking) HTTP requests.
//
// This trait is a supertrait of [`MaybeSend`] and [`MaybeSync`] for consistency with the
// [`AsyncHttpResolver`]. For more information on the rationale, see [`AsyncHttpResolver`].
//
// [`MaybeSend`]: crate::maybe_send_sync::MaybeSend
// [`MaybeSync`]: crate::maybe_send_sync::MaybeSync
pub trait SyncHttpResolver: MaybeSend + MaybeSync {
    /// Resolve a [`Request`] into a [`Response`] with a streaming body.
    ///
    /// [`Request`]: http::Request
    /// [`Response`]: http::Response
    fn http_resolve(
        &self,
        request: Request<Vec<u8>>,
    ) -> Result<Response<Box<dyn Read>>, HttpResolverError>;
}

/// This implementation is particularly useful for compatibility with the return
/// type of [`Context::resolver`].
///
/// [`Context::resolver`]: crate::Context::resolver
impl<T: SyncHttpResolver + ?Sized> SyncHttpResolver for Arc<T> {
    fn http_resolve(
        &self,
        request: Request<Vec<u8>>,
    ) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
        (**self).http_resolve(request)
    }
}

/// A resolver for non-blocking (async) HTTP requests.
//
// This trait is a supertrait of [`MaybeSend`] and [`MaybeSync`] because in many cases
// we use the pattern `&dyn AsyncHttpResolver`. For that to cross an await point, it
// must implement `Send`, and for that to happen, it must also implement `Sync`. Thus,
// rather than creating a new trait that combines `AsyncHttpResolver + MaybeSend + MaybeSync`,
// we require it here to reduce complexity.
//
// [`MaybeSend`]: crate::maybe_send_sync::MaybeSend
// [`MaybeSync`]: crate::maybe_send_sync::MaybeSync
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait AsyncHttpResolver: MaybeSend + MaybeSync {
    /// Resolve a [`Request`] into a [`Response`] with a streaming body.
    ///
    /// [`Request`]: http::Request
    /// [`Response`]: http::Response
    async fn http_resolve_async(
        &self,
        request: Request<Vec<u8>>,
    ) -> Result<Response<Box<dyn Read>>, HttpResolverError>;
}

/// This implementation is particularly useful for compatibility with the return
/// type of [`Context::resolver_async`].
///
/// [`Context::resolver_async`]: crate::Context::resolver_async
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl<T: AsyncHttpResolver + ?Sized> AsyncHttpResolver for Arc<T> {
    async fn http_resolve_async(
        &self,
        request: Request<Vec<u8>>,
    ) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
        (**self).http_resolve_async(request).await
    }
}

/// A generic resolver for [`SyncHttpResolver`].
///
/// This implementation will automatically choose a [`SyncHttpResolver`] based on the
/// enabled features:
/// * `ureq` - use `ureq::Agent`.
/// * `reqwest_blocking` - use `reqwest::blocking::Client`.
/// * `wasi` (WASI-only) - use `wasi::http::outgoing_handler::handle`.
///
/// This resolver is a pure HTTP client wrapper with no domain-specific logic.
/// For host filtering or other access control, wrap this with [`RestrictedResolver`].
///
/// Note that WASM (non-WASI) does not have a built-in [`SyncHttpResolver`].
///
/// [`RestrictedResolver`]: restricted::RestrictedResolver
pub struct SyncGenericResolver {
    inner: sync_resolver::Impl,
}

impl SyncGenericResolver {
    /// Create a new [`SyncGenericResolver`] with an auto-specified [`SyncHttpResolver`].
    ///
    /// This function will create a [`SyncHttpResolver`] that returns [`HttpResolverError::SyncHttpResolverNotImplemented`]
    /// under any of the following conditions:
    /// * If both `http_reqwest_blocking` and `http_ureq` aren't enabled.
    /// * If the platform is WASM.
    /// * If the platform is WASI and `http_wasi` isn't enabled.
    pub fn new() -> Self {
        Self {
            inner: sync_resolver::new(),
        }
    }

    /// Create a new [`SyncGenericResolver`] with an auto-specified [`SyncHttpResolver`] that has
    /// redirects enabled. Returns `None` if unsupported for the enabled HTTP resolver features.
    ///
    /// For more information, see [`SyncGenericResolver::new`].
    pub fn with_redirects() -> Option<Self> {
        sync_resolver::with_redirects().map(|inner| Self { inner })
    }
}

impl Default for SyncGenericResolver {
    fn default() -> Self {
        Self::new()
    }
}

impl SyncHttpResolver for SyncGenericResolver {
    fn http_resolve(
        &self,
        request: Request<Vec<u8>>,
    ) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
        self.inner.http_resolve(request)
    }
}

/// A generic resolver for [`AsyncHttpResolver`].
///
/// This implementation will automatically choose a [`AsyncHttpResolver`] based on the
/// enabled features:
/// * `reqwest` - use `reqwest::Client`.
/// * `wstd` (WASI-only) - use `wstd::http::Client`.
///
/// This resolver is a pure HTTP client wrapper with no domain-specific logic.
/// For host filtering or other access control, wrap this with [`RestrictedResolver`].
///
/// [`RestrictedResolver`]: restricted::RestrictedResolver
pub struct AsyncGenericResolver {
    inner: async_resolver::Impl,
    max_response_body_size: Option<u64>,
}

impl AsyncGenericResolver {
    /// Create a new [`AsyncGenericResolver`] with an auto-specified [`AsyncHttpResolver`].
    ///
    /// This function will create a [`AsyncHttpResolver`] that returns [`HttpResolverError::AsyncHttpResolverNotImplemented`]
    /// under any of the following conditions:
    /// * If `http_reqwest` isn't enabled.
    /// * If the platform is WASI and `http_wstd` isn't enabled.
    pub fn new() -> Self {
        Self {
            inner: async_resolver::new(),
            max_response_body_size: None,
        }
    }

    /// Create a new [`AsyncGenericResolver`] with an auto-specified [`AsyncHttpResolver`] that has
    /// redirects enabled. Returns `None` if unsupported for the enabled HTTP resolver features.
    ///
    /// For more information, see [`AsyncGenericResolver::new`].
    pub fn with_redirects() -> Option<Self> {
        async_resolver::with_redirects().map(|inner| Self {
            inner,
            max_response_body_size: None,
        })
    }

    /// Set the maximum number of bytes accepted from an HTTP response body.
    ///
    /// When set, [`http_resolve_async`] checks the `Content-Length` response header before
    /// returning the response. If the advertised size exceeds `limit`, it returns
    /// [`HttpResolverError::ResponseTooLarge`] immediately, without reading the body.
    ///
    /// This is an opt-in guard; the default is no limit (`None`).
    ///
    /// [`http_resolve_async`]: AsyncHttpResolver::http_resolve_async
    pub fn with_max_response_body_size(mut self, limit: u64) -> Self {
        self.max_response_body_size = Some(limit);
        self
    }
}

impl Default for AsyncGenericResolver {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl AsyncHttpResolver for AsyncGenericResolver {
    async fn http_resolve_async(
        &self,
        request: Request<Vec<u8>>,
    ) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
        let response = self.inner.http_resolve_async(request).await?;

        if let Some(limit) = self.max_response_body_size {
            // Fast-fail if Content-Length exceeds the limit before reading body bytes.
            let reported_len = response
                .headers()
                .get(http::header::CONTENT_LENGTH)
                .and_then(|v| v.to_str().ok())
                .and_then(|s| s.parse::<u64>().ok());
            if let Some(len) = reported_len {
                if len > limit {
                    return Err(HttpResolverError::ResponseTooLarge);
                }
            }

            // Read the body with a hard cap so servers that omit or lie about
            // Content-Length are also bounded.
            let (parts, body) = response.into_parts();
            let mut buf = Vec::new();
            body.take(limit + 1).read_to_end(&mut buf)?;
            if buf.len() as u64 > limit {
                return Err(HttpResolverError::ResponseTooLarge);
            }
            return Ok(http::Response::from_parts(
                parts,
                Box::new(Cursor::new(buf)) as Box<dyn Read>,
            ));
        }

        Ok(response)
    }
}

/// An error that occurs during sync/async HTTP resolver resolution.
#[derive(Debug, thiserror::Error)]
pub enum HttpResolverError {
    /// An error occurred in the [`http`] crate.
    #[error(transparent)]
    Http(#[from] http::Error),

    /// An error occurred in during I/O.
    #[error(transparent)]
    Io(#[from] io::Error),

    /// The sync HTTP resolver is not implemented.
    ///
    /// Note this often occurs when the http-related features are improperly enabled.
    #[error("the sync http resolver is not implemented")]
    SyncHttpResolverNotImplemented,

    /// The async HTTP resolver is not implemented.
    ///
    /// Note this often occurs when the http-related features are improperly enabled.
    #[error("the async http resolver is not implemented")]
    AsyncHttpResolverNotImplemented,

    /// The remote URI is blocked by the allowed list.
    ///
    /// The allowed list can be set via:
    /// - [`RestrictedResolver`] (for wrapping custom resolvers)
    /// - SDK settings via [`Core::allowed_network_hosts`]
    ///
    /// [`RestrictedResolver`]: restricted::RestrictedResolver
    /// [`Core::allowed_network_hosts`]: crate::settings::Core::allowed_network_hosts
    #[error("remote URI \"{uri}\" is not permitted by the allowed list")]
    UriDisallowed { uri: String },

    /// The `Content-Length` response header exceeded the limit set via
    /// [`AsyncGenericResolver::with_max_response_body_size`].
    #[error("response body exceeded maximum allowed size")]
    ResponseTooLarge,

    /// An error occurred from the underlying HTTP resolver.
    #[error("an error occurred from the underlying http resolver: {0}")]
    Other(Box<dyn std::error::Error + Send + Sync>),
}

#[cfg(all(
    not(target_arch = "wasm32"),
    feature = "http_reqwest_blocking",
    not(feature = "http_ureq")
))]
mod sync_resolver {
    pub use crate::http::reqwest::sync_impl::{new, with_redirects, Impl};
}

#[cfg(all(not(target_arch = "wasm32"), feature = "http_ureq"))]
mod sync_resolver {
    pub use crate::http::ureq::sync_impl::{new, with_redirects, Impl};
}

#[cfg(all(target_os = "wasi", feature = "http_wasi"))]
mod sync_resolver {
    pub use crate::http::wasi::sync_impl::{new, with_redirects, Impl};
}

#[cfg(not(any(
    all(target_os = "wasi", feature = "http_wasi"),
    all(
        not(target_arch = "wasm32"),
        any(feature = "http_ureq", feature = "http_reqwest_blocking")
    )
)))]
mod sync_resolver {
    use super::*;

    pub type Impl = SyncNoopResolver;
    pub fn new() -> Impl {
        SyncNoopResolver
    }
    pub fn with_redirects() -> Option<Impl> {
        Some(SyncNoopResolver)
    }

    pub struct SyncNoopResolver;

    impl SyncHttpResolver for SyncNoopResolver {
        fn http_resolve(
            &self,
            _request: Request<Vec<u8>>,
        ) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
            Err(HttpResolverError::SyncHttpResolverNotImplemented)
        }
    }
}

#[cfg(all(not(target_os = "wasi"), feature = "http_reqwest"))]
mod async_resolver {
    pub use crate::http::reqwest::async_impl::{new, with_redirects, Impl};
}

#[cfg(all(target_os = "wasi", feature = "http_wstd"))]
mod async_resolver {
    pub use crate::http::wasi::async_impl::{new, with_redirects, Impl};
}

#[cfg(not(any(
    feature = "http_reqwest",
    all(target_os = "wasi", feature = "http_wstd")
)))]
mod async_resolver {
    use super::*;

    pub type Impl = AsyncNoopResolver;
    pub fn new() -> Impl {
        AsyncNoopResolver
    }
    pub fn with_redirects() -> Option<Impl> {
        Some(AsyncNoopResolver)
    }

    pub struct AsyncNoopResolver;

    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
    impl AsyncHttpResolver for AsyncNoopResolver {
        async fn http_resolve_async(
            &self,
            _request: Request<Vec<u8>>,
        ) -> Result<Response<Box<dyn Read>>, HttpResolverError> {
            Err(HttpResolverError::AsyncHttpResolverNotImplemented)
        }
    }
}

// TODO: Use `httpmock` when it's supported for WASM https://github.com/contentauth/c2pa-rs/issues/1378
//       And then also implement `wasi`/`wstd` networking tests.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
pub mod tests {
    #![allow(clippy::unwrap_used)]

    use async_generic::async_generic;

    use super::*;

    fn mock_server<'a>(server: &'a httpmock::MockServer) -> httpmock::Mock<'a> {
        server.mock(|when, then| {
            when.method(httpmock::Method::GET);
            then.status(200).body([1, 2, 3]);
        })
    }

    fn redirect_mock_server<'a>(server: &'a httpmock::MockServer) -> httpmock::Mock<'a> {
        server.mock(|when, then| {
            when.method(httpmock::Method::GET).path("/redirect");
            then.status(302).header("Location", "/").body([3, 2, 1]);
        })
    }

    #[async_generic(async_signature(resolver: impl AsyncHttpResolver))]
    pub fn assert_http_resolver(resolver: impl SyncHttpResolver) {
        use httpmock::MockServer;

        let server = MockServer::start();
        let mock = mock_server(&server);

        let request = Request::get(server.base_url()).body(vec![1, 2, 3]).unwrap();

        let response = if _sync {
            resolver.http_resolve(request).unwrap()
        } else {
            resolver.http_resolve_async(request).await.unwrap()
        };

        let mut response_body = Vec::new();
        response
            .into_body()
            .read_to_end(&mut response_body)
            .unwrap();
        assert_eq!(&response_body, &[1, 2, 3]);

        mock.assert();
    }

    #[async_generic(async_signature(resolver: impl AsyncHttpResolver))]
    pub fn assert_http_resolver_with_redirects(resolver: impl SyncHttpResolver) {
        use httpmock::MockServer;

        let server = MockServer::start();
        let redirect = redirect_mock_server(&server);
        let target = mock_server(&server);

        let request = Request::get(format!("{}/redirect", server.base_url()))
            .body(vec![3, 2, 1])
            .unwrap();

        let response = if _sync {
            resolver.http_resolve(request).unwrap()
        } else {
            resolver.http_resolve_async(request).await.unwrap()
        };

        assert_eq!(response.status(), 200);
        redirect.assert_calls(1);
        target.assert_calls(1);
    }
}
