use std::time::Duration;

const MAX_NUM_CHANNELS: usize = 200;

/// Low-level gRPC settings for communicating with Momento.
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct GrpcConfiguration {
    /// The duration the client is willing to wait for an RPC to complete before it is terminated
    /// with a DeadlineExceeded error.
    pub(crate) deadline: Duration,
    /// The number of grpc channels (TCP connections) to create
    pub(crate) num_channels: usize,
    /// Indicates whether the client should send keep-alive pings.
    ///
    /// NOTE: keep-alives are very important for long-lived server environments where there may be
    /// periods of time when the connection is idle. However, they are very problematic for lambda
    /// environments where the lambda runtime is continuously frozen and unfrozen, because the
    /// lambda may be frozen before the "ACK" is received from the server. This can cause the
    /// keep-alive to timeout even though the connection is completely healthy. Therefore,
    /// keep-alives should be disabled in lambda and similar environments.
    pub(crate) keep_alive_while_idle: Option<bool>,
    /// The interval at which keep-alive pings are sent.
    pub(crate) keep_alive_interval: Option<Duration>,
    /// The duration the client is willing to wait for a keep-alive ping to be acknowledged before
    /// closing the connection.
    pub(crate) keep_alive_timeout: Option<Duration>,
    /// The maximum size of messages that can be sent to the server. If the client attempts to send a message larger than
    /// this size, it will result in a LimitExceededError.
    pub(crate) max_send_message_size: Option<usize>,
    /// The maximum size of messages that can be received from the server. If the server sends a message larger than
    /// this size, it will result in a LimitExceededError.
    pub(crate) max_receive_message_size: Option<usize>,
}

impl GrpcConfiguration {
    /// Constructs a new GrpcConfigurationBuilder in the NeedsDeadline state.
    pub fn builder() -> GrpcConfigurationBuilder<NeedsDeadline> {
        GrpcConfigurationBuilder(NeedsDeadline(()))
    }
}

/// The initial state of the GrpcConfigurationBuilder.
pub struct GrpcConfigurationBuilder<State>(State);

/// The state of the GrpcConfigurationBuilder when it is waiting for the RPC deadline to be set.
pub struct NeedsDeadline(());

/// The state of the GrpcConfigurationBuilder when it is ready to build a GrpcConfiguration.
pub struct ReadyToBuild {
    deadline: Duration,
    num_channels: usize,
    keep_alive_while_idle: Option<bool>,
    keep_alive_interval: Option<Duration>,
    keep_alive_timeout: Option<Duration>,
    max_send_message_size: Option<usize>,
    max_receive_message_size: Option<usize>,
}

impl GrpcConfigurationBuilder<NeedsDeadline> {
    /// Sets the duration the client is willing to wait for an RPC to complete before it is
    /// terminated with a DeadlineExceeded error.
    pub fn deadline(self, deadline: Duration) -> GrpcConfigurationBuilder<ReadyToBuild> {
        GrpcConfigurationBuilder(ReadyToBuild {
            deadline,
            num_channels: 1,
            keep_alive_while_idle: None,
            keep_alive_interval: None,
            keep_alive_timeout: None,
            max_send_message_size: None,
            max_receive_message_size: None,
        })
    }
}

impl GrpcConfigurationBuilder<ReadyToBuild> {
    pub(crate) fn num_channels(mut self, num_channels: usize) -> Self {
        self.0.num_channels = num_channels;
        self
    }

    pub(crate) fn enable_keep_alives_with_defaults(
        mut self,
    ) -> GrpcConfigurationBuilder<ReadyToBuild> {
        self.0.keep_alive_while_idle = Some(true);
        self.0.keep_alive_interval = Some(Duration::from_secs(5));
        self.0.keep_alive_timeout = Some(Duration::from_secs(1));
        self
    }

    /// Indicates whether the client should send keep-alive pings.
    ///
    /// NOTE: keep-alives are very important for long-lived server environments where there may be periods of time
    /// when the connection is idle. However, they are very problematic for lambda environments where the lambda
    /// runtime is continuously frozen and unfrozen, because the lambda may be frozen before the "ACK" is received
    /// from the server. This can cause the keep-alive to timeout even though the connection is completely healthy.
    /// Therefore, keep-alives should be disabled in lambda and similar environments.
    pub fn keep_alive_while_idle(mut self, keep_alive_while_idle: bool) -> Self {
        self.0.keep_alive_while_idle = Some(keep_alive_while_idle);
        self
    }

    /// The interval at which keep-alive pings are sent.
    ///
    /// NOTE: keep-alives are very important for long-lived server environments where there may be periods of time
    /// when the connection is idle. However, they are very problematic for lambda environments where the lambda
    /// runtime is continuously frozen and unfrozen, because the lambda may be frozen before the "ACK" is received
    /// from the server. This can cause the keep-alive to timeout even though the connection is completely healthy.
    /// Therefore, keep-alives should be disabled in lambda and similar environments.
    pub fn keep_alive_interval(mut self, keep_alive_interval: Duration) -> Self {
        self.0.keep_alive_interval = Some(keep_alive_interval);
        self
    }

    /// The duration the client is willing to wait for a keep-alive ping to be acknowledged before
    /// closing the connection.
    ///
    /// NOTE: keep-alives are very important for long-lived server environments where there may be periods of time
    /// when the connection is idle. However, they are very problematic for lambda environments where the lambda
    /// runtime is continuously frozen and unfrozen, because the lambda may be frozen before the "ACK" is received
    /// from the server. This can cause the keep-alive to timeout even though the connection is completely healthy.
    /// Therefore, keep-alives should be disabled in lambda and similar environments.
    pub fn keep_alive_timeout(mut self, keep_alive_timeout: Duration) -> Self {
        self.0.keep_alive_timeout = Some(keep_alive_timeout);
        self
    }

    /// Sets the maximum size of messages that can be sent to the server. If the client attempts to send a message larger than
    /// this size, it will result in a LimitExceededError.
    pub fn max_send_message_size(mut self, size: usize) -> Self {
        self.0.max_send_message_size = Some(size);
        self
    }

    /// Sets the maximum size of messages that can be received from the server. If the server sends a message larger than
    /// this size, it will result in a LimitExceededError.
    pub fn max_receive_message_size(mut self, size: usize) -> Self {
        self.0.max_receive_message_size = Some(size);
        self
    }

    /// Constructs the GrpcConfiguration with the given settings.
    pub fn build(self) -> GrpcConfiguration {
        let num_channels = self.0.num_channels;
        if num_channels == 0 || num_channels > MAX_NUM_CHANNELS {
            panic!(
                "The maximum number of grpc channels (connections must be between 1 and {}",
                MAX_NUM_CHANNELS
            );
        }
        GrpcConfiguration {
            deadline: self.0.deadline,
            num_channels: self.0.num_channels,
            keep_alive_while_idle: self.0.keep_alive_while_idle,
            keep_alive_interval: self.0.keep_alive_interval,
            keep_alive_timeout: self.0.keep_alive_timeout,
            max_send_message_size: self.0.max_send_message_size,
            max_receive_message_size: self.0.max_receive_message_size,
        }
    }
}

impl From<GrpcConfigurationBuilder<ReadyToBuild>> for GrpcConfiguration {
    fn from(builder: GrpcConfigurationBuilder<ReadyToBuild>) -> GrpcConfiguration {
        builder.build()
    }
}
