#[cfg(feature = "p2")]
use crate::p2::bindings::http::types as p2;
#[cfg(feature = "p3")]
use crate::p3::bindings::http::types as p3;
use crate::{DEFAULT_FORBIDDEN_HEADERS, Error, RequestOptions, Result};
use bytes::Bytes;
use http::{HeaderName, uri::Scheme};
use http_body_util::combinators::UnsyncBoxBody;
use wasmtime::component::{HasData, ResourceTable};

/// A helper struct which implements [`HasData`] for the `wasi:http` APIs.
///
/// This can be useful when directly calling `add_to_linker` functions directly,
/// such as [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`] as
/// the `D` type parameter. See [`HasData`] for more information about the type
/// parameter's purpose.
///
/// When using this type you can skip the [`WasiHttpView`] trait, for example.
///
/// [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`]: crate::p3::bindings::http::types::add_to_linker
///
/// # Examples
///
/// ```
/// use wasmtime::component::Linker;
/// use wasmtime::{Engine, Result};
/// use wasmtime_wasi_http::{WasiHttp, WasiHttpCtxView};
///
/// struct MyStoreState {
///     // ...
/// }
///
/// impl MyStoreState {
///     fn http(&mut self) -> WasiHttpCtxView<'_> {
///         // ...
/// #       todo!()
///     }
/// }
///
/// fn main() -> Result<()> {
///     let engine = Engine::default();
///     let mut linker = Linker::new(&engine);
///
///     wasmtime_wasi_http::p3::bindings::http::types::add_to_linker::<MyStoreState, WasiHttp>(
///         &mut linker,
///         |state| state.http(),
///     )?;
///     Ok(())
/// }
/// ```
pub struct WasiHttp;

impl HasData for WasiHttp {
    type Data<'a> = WasiHttpCtxView<'a>;
}

/// A trait which provides internal WASI HTTP state.
///
/// This trait is used by the [`add_to_linker`] convenience functions of this
/// crate. This trait can be implemented for the `T` in `Store<T>` to provide
/// access to wasi:http information at runtime.
///
/// [`add_to_linker`]: crate::p3::add_to_linker
///
/// # Example
///
/// ```
/// use wasmtime::component::ResourceTable;
/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView};
///
/// struct MyState {
///     http_ctx: WasiHttpCtx,
///     table: ResourceTable,
/// }
///
/// impl WasiHttpView for MyState {
///     fn http(&mut self) -> WasiHttpCtxView<'_> {
///         WasiHttpCtxView {
///             ctx: &mut self.http_ctx,
///             table: &mut self.table,
///             hooks: Default::default(),
///         }
///     }
/// }
/// ```
pub trait WasiHttpView: Send {
    /// Return a [WasiHttpCtxView] from mutable reference to self.
    fn http(&mut self) -> WasiHttpCtxView<'_>;
}

/// Basis of implementation of all `wasi:http` APIs in this crate.
///
/// This type provides a temporary view into information such as a WASI
/// resource table, HTTP context information, and embedder-provided hooks if
/// so desired. THe fields in this structure are typically stored within the
/// `T` of `Store<T>` and this struct borrows from there. All `Host` traits
/// generated by `bindgen!` are implemented for this type.
pub struct WasiHttpCtxView<'a> {
    /// Mutable reference to the WASI HTTP hooks.
    ///
    /// Note that [`default_hooks`] or [`Default::default()`] can be used if
    /// you don't want or need to customize this.
    pub hooks: &'a mut dyn WasiHttpHooks,

    /// Mutable reference to table used to manage resources.
    pub table: &'a mut ResourceTable,

    /// Mutable reference to the WASI HTTP context.
    pub ctx: &'a mut WasiHttpCtx,
}

/// Default maximum size for the contents of a fields resource.
///
/// Typically, HTTP proxies limit headers to 8k. This number is higher than that
/// because it not only includes the wire-size of headers but it additionally
/// includes factors for the in-memory representation of `HeaderMap`. This is in
/// theory high enough that no one runs into it but low enough such that a
/// completely full `HeaderMap` doesn't break the bank in terms of memory
/// consumption.
const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024;

/// Capture the state necessary for use in the wasi-http API implementation.
#[derive(Debug, Clone)]
pub struct WasiHttpCtx {
    pub(crate) field_size_limit: usize,
}

impl WasiHttpCtx {
    /// Create a new context.
    pub fn new() -> Self {
        Self {
            field_size_limit: DEFAULT_FIELD_SIZE_LIMIT,
        }
    }

    /// Set the maximum size for any fields resources created by this context.
    ///
    /// The limit specified here is roughly a byte limit for the size of the
    /// in-memory representation of headers. This means that the limit needs to
    /// be larger than the literal representation of headers on the wire to
    /// account for in-memory Rust-side data structures representing the header
    /// names/values/etc.
    pub fn set_field_size_limit(&mut self, limit: usize) {
        self.field_size_limit = limit;
    }
}

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

/// Convenience type definition for the bodies used in this crate.
pub type WasiBody = UnsyncBoxBody<Bytes, Error>;

/// A trait which provides hooks into internal WASI HTTP operations.
///
/// Note that when using this type if state is needed to implement the methods
/// the state will need to be stored separately in a distinct structure to
/// implement this trait as the same type can't implement both [`WasiHttpView`]
/// and [`WasiHttpHooks`] and be usable.
///
/// # Example
///
/// ```
/// use wasmtime::component::ResourceTable;
/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView, WasiHttpHooks};
///
/// struct MyState {
///     http_ctx: WasiHttpCtx,
///     table: ResourceTable,
///     hooks: MyHooks,
/// }
///
/// impl MyState {
///     fn new() -> MyState {
///         MyState {
///             table: ResourceTable::new(),
///             http_ctx: WasiHttpCtx::new(),
///             hooks: MyHooks,
///         }
///     }
/// }
///
/// impl WasiHttpView for MyState {
///     fn http(&mut self) -> WasiHttpCtxView<'_> {
///         WasiHttpCtxView {
///             ctx: &mut self.http_ctx,
///             table: &mut self.table,
///             hooks: &mut self.hooks,
///         }
///     }
/// }
///
/// struct MyHooks;
///
/// impl WasiHttpHooks for MyHooks {
///     fn is_forbidden_header(&mut self, name: &http::HeaderName) -> bool {
///         *name == http::header::AUTHORIZATION ||
///             wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS.contains(name)
///     }
/// }
/// ```
pub trait WasiHttpHooks: Send {
    /// Whether a given header should be considered forbidden and not allowed.
    fn is_forbidden_header(&mut self, name: &HeaderName) -> bool {
        DEFAULT_FORBIDDEN_HEADERS.contains(name)
    }

    /// Whether a given scheme should be considered supported.
    ///
    /// `handle` will return [Error::HttpProtocolError] for unsupported schemes.
    fn is_supported_scheme(&mut self, scheme: &Scheme) -> bool {
        *scheme == Scheme::HTTP || *scheme == Scheme::HTTPS
    }

    /// Whether to set `host` header in the request passed to `send_request`.
    fn set_host_header(&mut self) -> bool {
        true
    }

    /// Scheme to default to, when not set by the guest.
    ///
    /// If [None], `handle` will return [Error::HttpProtocolError]
    /// for requests missing a scheme.
    fn default_scheme(&mut self) -> Option<Scheme> {
        Some(Scheme::HTTPS)
    }

    /// Send an outgoing request.
    ///
    /// This function will be used by the `wasi:http/handler#handle` implementation.
    ///
    /// The specified [Future] `fut` will be used to communicate
    /// a response processing error, if any.
    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
    /// a result will be sent on `fut`.
    ///
    /// The returned [Future] can be used to communicate
    /// a request processing error, if any, to the constructor of the request.
    /// For example, if the request was constructed via `wasi:http/types.request#new`,
    /// a result resolved from it will be forwarded to the guest on the future handle returned.
    ///
    /// `Content-Length` of the request passed to this function will be validated, however no
    /// `Content-Length` validation will be performed for the received response.
    #[cfg(feature = "default-send-request")]
    fn send_request(
        &mut self,
        request: http::Request<WasiBody>,
        options: Option<RequestOptions>,
        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
    ) -> Box<
        dyn Future<
                Output = Result<(
                    http::Response<WasiBody>,
                    Box<dyn Future<Output = Result<(), Error>> + Send>,
                )>,
            > + Send,
    > {
        _ = fut;
        Box::new(async move {
            use http_body_util::BodyExt;

            let (res, io) = crate::default_send_request(request, options).await?;
            Ok((
                res.map(BodyExt::boxed_unsync),
                Box::new(io) as Box<dyn Future<Output = _> + Send>,
            ))
        })
    }

    /// Send an outgoing request.
    ///
    /// This function will be used by the `wasi:http/handler#handle` implementation.
    ///
    /// The specified [Future] `fut` will be used to communicate
    /// a response processing error, if any.
    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
    /// a result will be sent on `fut`.
    ///
    /// The returned [Future] can be used to communicate
    /// a request processing error, if any, to the constructor of the request.
    /// For example, if the request was constructed via `wasi:http/types.request#new`,
    /// a result resolved from it will be forwarded to the guest on the future handle returned.
    ///
    /// `Content-Length` of the request passed to this function will be validated, however no
    /// `Content-Length` validation will be performed for the received response.
    #[cfg(not(feature = "default-send-request"))]
    fn send_request(
        &mut self,
        request: http::Request<WasiBody>,
        options: Option<RequestOptions>,
        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
    ) -> Box<
        dyn Future<
                Output = Result<(
                    http::Response<WasiBody>,
                    Box<dyn Future<Output = Result<(), Error>> + Send>,
                )>,
            > + Send,
    >;

    /// Number of distinct write calls to the outgoing body's output-stream
    /// that the implementation will buffer.
    /// Default: 1.
    #[cfg(feature = "p2")]
    fn p2_outgoing_body_buffer_chunks(&mut self) -> usize {
        crate::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS
    }

    /// Maximum size allowed in a write call to the outgoing body's
    /// output-stream.  Default: 1024 * 1024.
    #[cfg(feature = "p2")]
    fn p2_outgoing_body_chunk_size(&mut self) -> usize {
        crate::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
    }

    /// Optional hook to configure the error code for hyper errors.
    #[cfg(feature = "p2")]
    fn p2_error_from_hyper(&mut self, err: &hyper::Error) -> p2::ErrorCode {
        tracing::warn!("hyper error: {err:?}");
        p2::ErrorCode::HttpProtocolError
    }

    /// Optional hook to configure the error code for connect I/O errors.
    #[cfg(feature = "p2")]
    fn p2_error_from_connect(&mut self, err: &std::io::Error) -> p2::ErrorCode {
        tracing::warn!("connect error: {err:?}");
        p2::ErrorCode::ConnectionRefused
    }

    /// Optional hook to configure the error code for TLS I/O errors.
    #[cfg(feature = "p2")]
    fn p2_error_from_tls(&mut self, err: &std::io::Error) -> p2::ErrorCode {
        tracing::warn!("tls error: {err:?}");
        p2::ErrorCode::TlsProtocolError
    }

    /// Optional hook to configure the error code for DNS errors.
    #[cfg(all(feature = "p2", feature = "default-send-request"))]
    fn p2_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p2::ErrorCode {
        tracing::warn!("dns lookup error: {err:?}");
        p2::ErrorCode::DnsError(p2::DnsErrorPayload {
            rcode: Some("invalid dns name".to_string()),
            info_code: None,
        })
    }

    /// Maximum number of bytes the implementation will copy out of the guest in
    /// a single write to an outgoing body's stream.
    #[cfg(feature = "p3")]
    fn p3_outgoing_body_chunk_size(&mut self) -> usize {
        crate::p3::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
    }

    /// Optional hook to configure the error code for hyper errors.
    #[cfg(feature = "p3")]
    fn p3_error_from_hyper(&mut self, err: &hyper::Error) -> p3::ErrorCode {
        tracing::warn!("hyper error: {err:?}");
        p3::ErrorCode::HttpProtocolError
    }

    /// Optional hook to configure the error code for connect I/O errors.
    #[cfg(feature = "p3")]
    fn p3_error_from_connect(&mut self, err: &std::io::Error) -> p3::ErrorCode {
        tracing::warn!("connect error: {err:?}");
        p3::ErrorCode::ConnectionRefused
    }

    /// Optional hook to configure the error code for TLS I/O errors.
    #[cfg(feature = "p3")]
    fn p3_error_from_tls(&mut self, err: &std::io::Error) -> p3::ErrorCode {
        tracing::warn!("tls error: {err:?}");
        p3::ErrorCode::TlsProtocolError
    }

    /// Optional hook to configure the error code for DNS errors.
    #[cfg(all(feature = "p3", feature = "default-send-request"))]
    fn p3_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p3::ErrorCode {
        tracing::warn!("dns lookup error: {err:?}");
        p3::ErrorCode::DnsError(p3::DnsErrorPayload {
            rcode: Some("invalid dns name".to_string()),
            info_code: None,
        })
    }
}

/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has
/// the default behavior for `wasi:http`.
#[cfg(feature = "default-send-request")]
pub fn default_hooks() -> &'static mut dyn WasiHttpHooks {
    Default::default()
}

#[cfg(feature = "default-send-request")]
impl<'a> Default for &'a mut dyn WasiHttpHooks {
    fn default() -> Self {
        let x: &mut [(); 0] = &mut [];
        x
    }
}

#[doc(hidden)]
#[cfg(feature = "default-send-request")]
impl WasiHttpHooks for [(); 0] {}
