use std::{fmt, marker::PhantomData, net, rc::Rc, time::Duration};

use actix_codec::Framed;
use actix_service::{IntoServiceFactory, Service, ServiceFactory};

use crate::{
    body::{BoxBody, MessageBody},
    config::{
        DEFAULT_H1_WRITE_BUFFER_SIZE, DEFAULT_H2_CONN_WINDOW_SIZE, DEFAULT_H2_STREAM_WINDOW_SIZE,
    },
    h1::{self, ExpectHandler, H1Service, UpgradeHandler},
    service::HttpService,
    ConnectCallback, Extensions, KeepAlive, Request, Response, ServiceConfigBuilder,
};

/// An HTTP service builder.
///
/// This type can construct an instance of [`HttpService`] through a builder-like pattern.
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
    keep_alive: KeepAlive,
    client_request_timeout: Duration,
    client_disconnect_timeout: Duration,
    tcp_nodelay: Option<bool>,
    secure: bool,
    local_addr: Option<net::SocketAddr>,
    h1_allow_half_closed: bool,
    h1_write_buffer_size: usize,
    h2_conn_window_size: u32,
    h2_stream_window_size: u32,
    expect: X,
    upgrade: Option<U>,
    on_connect_ext: Option<Rc<ConnectCallback<T>>>,
    _phantom: PhantomData<S>,
}

impl<T, S> Default for HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
where
    S: ServiceFactory<Request, Config = ()>,
    S::Error: Into<Response<BoxBody>> + 'static,
    S::InitError: fmt::Debug,
    <S::Service as Service<Request>>::Future: 'static,
{
    fn default() -> Self {
        HttpServiceBuilder {
            // ServiceConfig parts (make sure defaults match)
            keep_alive: KeepAlive::default(),
            client_request_timeout: Duration::from_secs(5),
            client_disconnect_timeout: Duration::ZERO,
            tcp_nodelay: None,
            secure: false,
            local_addr: None,
            h1_allow_half_closed: true,
            h1_write_buffer_size: DEFAULT_H1_WRITE_BUFFER_SIZE,
            h2_conn_window_size: DEFAULT_H2_CONN_WINDOW_SIZE,
            h2_stream_window_size: DEFAULT_H2_STREAM_WINDOW_SIZE,

            // dispatcher parts
            expect: ExpectHandler,
            upgrade: None,
            on_connect_ext: None,
            _phantom: PhantomData,
        }
    }
}

impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where
    S: ServiceFactory<Request, Config = ()>,
    S::Error: Into<Response<BoxBody>> + 'static,
    S::InitError: fmt::Debug,
    <S::Service as Service<Request>>::Future: 'static,
    X: ServiceFactory<Request, Config = (), Response = Request>,
    X::Error: Into<Response<BoxBody>>,
    X::InitError: fmt::Debug,
    U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
    U::Error: fmt::Display,
    U::InitError: fmt::Debug,
{
    /// Set connection keep-alive setting.
    ///
    /// Applies to HTTP/1.1 keep-alive and HTTP/2 ping-pong.
    ///
    /// By default keep-alive is 5 seconds.
    pub fn keep_alive<W: Into<KeepAlive>>(mut self, val: W) -> Self {
        self.keep_alive = val.into();
        self
    }

    /// Set connection secure state
    pub fn secure(mut self) -> Self {
        self.secure = true;
        self
    }

    /// Set the local address that this service is bound to.
    pub fn local_addr(mut self, addr: net::SocketAddr) -> Self {
        self.local_addr = Some(addr);
        self
    }

    /// Set client request timeout (for first request).
    ///
    /// Defines a timeout for reading client request header. If the client does not transmit the
    /// request head within this duration, the connection is terminated with a `408 Request Timeout`
    /// response error.
    ///
    /// A duration of zero disables the timeout.
    ///
    /// By default, the client timeout is 5 seconds.
    pub fn client_request_timeout(mut self, dur: Duration) -> Self {
        self.client_request_timeout = dur;
        self
    }

    #[doc(hidden)]
    #[deprecated(since = "3.0.0", note = "Renamed to `client_request_timeout`.")]
    pub fn client_timeout(self, dur: Duration) -> Self {
        self.client_request_timeout(dur)
    }

    /// Set client connection disconnect timeout.
    ///
    /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete
    /// within this time, the request get dropped. This timeout affects secure connections.
    ///
    /// A duration of zero disables the timeout.
    ///
    /// By default, the disconnect timeout is disabled.
    pub fn client_disconnect_timeout(mut self, dur: Duration) -> Self {
        self.client_disconnect_timeout = dur;
        self
    }

    /// Sets `TCP_NODELAY` value on accepted TCP connections.
    pub fn tcp_nodelay(mut self, nodelay: bool) -> Self {
        self.tcp_nodelay = Some(nodelay);
        self
    }

    #[doc(hidden)]
    #[deprecated(since = "3.0.0", note = "Renamed to `client_disconnect_timeout`.")]
    pub fn client_disconnect(self, dur: Duration) -> Self {
        self.client_disconnect_timeout(dur)
    }

    /// Sets whether HTTP/1 connections should support half-closures.
    ///
    /// Clients can choose to shutdown their writer-side of the connection after completing their
    /// request and while waiting for the server response. Setting this to `false` will cause the
    /// server to abort the connection handling as soon as it detects an EOF from the client.
    ///
    /// The default behavior is to allow, i.e. `true`
    pub fn h1_allow_half_closed(mut self, allow: bool) -> Self {
        self.h1_allow_half_closed = allow;
        self
    }

    /// Sets the maximum response write buffer size for HTTP/1 connections.
    ///
    /// Once the response buffer reaches this size, the dispatcher flushes it to the I/O stream.
    ///
    /// The default value is 32 KiB.
    ///
    /// # Panics
    ///
    /// Panics if `size` is 0.
    pub fn h1_write_buffer_size(mut self, size: usize) -> Self {
        assert!(
            size > 0,
            "HTTP/1 write buffer size must be greater than zero"
        );

        self.h1_write_buffer_size = size;
        self
    }

    /// Sets initial stream-level flow control window size for HTTP/2 connections.
    ///
    /// See [`ServiceConfigBuilder::h2_initial_window_size`] for more details.
    pub fn h2_initial_window_size(mut self, size: u32) -> Self {
        self.h2_stream_window_size = size;
        self
    }

    /// Sets initial connection-level flow control window size for HTTP/2 connections.
    ///
    /// See [`ServiceConfigBuilder::h2_initial_connection_window_size`] for more details.
    pub fn h2_initial_connection_window_size(mut self, size: u32) -> Self {
        self.h2_conn_window_size = size;
        self
    }

    /// Provide service for `EXPECT: 100-Continue` support.
    ///
    /// Service get called with request that contains `EXPECT` header.
    /// Service must return request in case of success, in that case
    /// request will be forwarded to main service.
    pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U>
    where
        F: IntoServiceFactory<X1, Request>,
        X1: ServiceFactory<Request, Config = (), Response = Request>,
        X1::Error: Into<Response<BoxBody>>,
        X1::InitError: fmt::Debug,
    {
        HttpServiceBuilder {
            keep_alive: self.keep_alive,
            client_request_timeout: self.client_request_timeout,
            client_disconnect_timeout: self.client_disconnect_timeout,
            tcp_nodelay: self.tcp_nodelay,
            secure: self.secure,
            local_addr: self.local_addr,
            h1_allow_half_closed: self.h1_allow_half_closed,
            h1_write_buffer_size: self.h1_write_buffer_size,
            h2_conn_window_size: self.h2_conn_window_size,
            h2_stream_window_size: self.h2_stream_window_size,
            expect: expect.into_factory(),
            upgrade: self.upgrade,
            on_connect_ext: self.on_connect_ext,
            _phantom: PhantomData,
        }
    }

    /// Provide service for custom `Connection: UPGRADE` support.
    ///
    /// If service is provided then normal requests handling get halted
    /// and this service get called with original request and framed object.
    pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
    where
        F: IntoServiceFactory<U1, (Request, Framed<T, h1::Codec>)>,
        U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
        U1::Error: fmt::Display,
        U1::InitError: fmt::Debug,
    {
        HttpServiceBuilder {
            keep_alive: self.keep_alive,
            client_request_timeout: self.client_request_timeout,
            client_disconnect_timeout: self.client_disconnect_timeout,
            tcp_nodelay: self.tcp_nodelay,
            secure: self.secure,
            local_addr: self.local_addr,
            h1_allow_half_closed: self.h1_allow_half_closed,
            h1_write_buffer_size: self.h1_write_buffer_size,
            h2_conn_window_size: self.h2_conn_window_size,
            h2_stream_window_size: self.h2_stream_window_size,
            expect: self.expect,
            upgrade: Some(upgrade.into_factory()),
            on_connect_ext: self.on_connect_ext,
            _phantom: PhantomData,
        }
    }

    /// Sets the callback to be run on connection establishment.
    ///
    /// Has mutable access to a data container that will be merged into request extensions.
    /// This enables transport layer data (like client certificates) to be accessed in middleware
    /// and handlers.
    pub fn on_connect_ext<F>(mut self, f: F) -> Self
    where
        F: Fn(&T, &mut Extensions) + 'static,
    {
        self.on_connect_ext = Some(Rc::new(f));
        self
    }

    /// Finish service configuration and create a service for the HTTP/1 protocol.
    pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
    where
        B: MessageBody,
        F: IntoServiceFactory<S, Request>,
        S::Error: Into<Response<BoxBody>>,
        S::InitError: fmt::Debug,
        S::Response: Into<Response<B>>,
    {
        let cfg = ServiceConfigBuilder::new()
            .keep_alive(self.keep_alive)
            .client_request_timeout(self.client_request_timeout)
            .client_disconnect_timeout(self.client_disconnect_timeout)
            .tcp_nodelay(self.tcp_nodelay)
            .secure(self.secure)
            .local_addr(self.local_addr)
            .h1_allow_half_closed(self.h1_allow_half_closed)
            .h1_write_buffer_size(self.h1_write_buffer_size)
            .h2_initial_window_size(self.h2_stream_window_size)
            .h2_initial_connection_window_size(self.h2_conn_window_size)
            .build();

        H1Service::with_config(cfg, service.into_factory())
            .expect(self.expect)
            .upgrade(self.upgrade)
            .on_connect_ext(self.on_connect_ext)
    }

    /// Finish service configuration and create a service for the HTTP/2 protocol.
    #[cfg(feature = "http2")]
    pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
    where
        F: IntoServiceFactory<S, Request>,
        S::Error: Into<Response<BoxBody>> + 'static,
        S::InitError: fmt::Debug,
        S::Response: Into<Response<B>> + 'static,

        B: MessageBody + 'static,
    {
        let cfg = ServiceConfigBuilder::new()
            .keep_alive(self.keep_alive)
            .client_request_timeout(self.client_request_timeout)
            .client_disconnect_timeout(self.client_disconnect_timeout)
            .tcp_nodelay(self.tcp_nodelay)
            .secure(self.secure)
            .local_addr(self.local_addr)
            .h1_allow_half_closed(self.h1_allow_half_closed)
            .h1_write_buffer_size(self.h1_write_buffer_size)
            .h2_initial_window_size(self.h2_stream_window_size)
            .h2_initial_connection_window_size(self.h2_conn_window_size)
            .build();

        crate::h2::H2Service::with_config(cfg, service.into_factory())
            .on_connect_ext(self.on_connect_ext)
    }

    /// Finish service configuration and create `HttpService` instance.
    pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
    where
        F: IntoServiceFactory<S, Request>,
        S::Error: Into<Response<BoxBody>> + 'static,
        S::InitError: fmt::Debug,
        S::Response: Into<Response<B>> + 'static,

        B: MessageBody + 'static,
    {
        let cfg = ServiceConfigBuilder::new()
            .keep_alive(self.keep_alive)
            .client_request_timeout(self.client_request_timeout)
            .client_disconnect_timeout(self.client_disconnect_timeout)
            .tcp_nodelay(self.tcp_nodelay)
            .secure(self.secure)
            .local_addr(self.local_addr)
            .h1_allow_half_closed(self.h1_allow_half_closed)
            .h1_write_buffer_size(self.h1_write_buffer_size)
            .h2_initial_window_size(self.h2_stream_window_size)
            .h2_initial_connection_window_size(self.h2_conn_window_size)
            .build();

        HttpService::with_config(cfg, service.into_factory())
            .expect(self.expect)
            .upgrade(self.upgrade)
            .on_connect_ext(self.on_connect_ext)
    }
}
