use bytes::Bytes;
use tracing::trace;

use crate::frame::Close;
use crate::{ApplicationClose, ConnectionClose, ConnectionError, TransportError, TransportErrorCode};

#[allow(unreachable_pub)] // fuzzing only
#[derive(Debug, Clone)]
pub struct State {
    /// Nested [`InnerState`] to enforce all state transitions are done in this module.
    inner: InnerState,
}

impl State {
    pub(super) fn as_handshake_mut(&mut self) -> Option<&mut Handshake> {
        if let InnerState::Handshake(ref mut hs) = self.inner {
            Some(hs)
        } else {
            None
        }
    }

    pub(super) fn as_handshake(&self) -> Option<&Handshake> {
        if let InnerState::Handshake(ref hs) = self.inner {
            Some(hs)
        } else {
            None
        }
    }

    pub(super) fn as_closed(&self) -> Option<&CloseReason> {
        if let InnerState::Closed {
            ref remote_reason, ..
        } = self.inner
        {
            Some(remote_reason)
        } else {
            None
        }
    }

    #[allow(unreachable_pub)] // fuzzing only
    #[cfg(any(test, fuzzing))]
    pub fn established() -> Self {
        Self {
            inner: InnerState::Established,
        }
    }

    pub(super) fn handshake(hs: Handshake) -> Self {
        Self {
            inner: InnerState::Handshake(hs),
        }
    }

    pub(super) fn move_to_handshake(&mut self, hs: Handshake) {
        self.inner = InnerState::Handshake(hs);
        trace!("connection state: handshake");
    }

    pub(super) fn move_to_established(&mut self) {
        self.inner = InnerState::Established;
        trace!("connection state: established");
    }

    /// Moves to the drained state.
    ///
    /// Panics if the state was already drained.
    ///
    /// Returns whether we were in the draining state before.
    pub(super) fn move_to_drained(&mut self, error: Option<ConnectionError>) -> bool {
        let (error, is_local, was_draining) = if let Some(error) = error {
            (
                Some(error),
                false,
                matches!(self.inner, InnerState::Draining { .. }),
            )
        } else {
            let (error, was_draining) = match &mut self.inner {
                InnerState::Draining { error, .. } => (error.take(), true),
                InnerState::Drained { .. } => panic!("invalid state transition drained -> drained"),
                InnerState::Closed { error_read, .. } if *error_read => (None, false),
                InnerState::Closed { remote_reason, .. } => {
                    let error = match remote_reason.clone().into() {
                        ConnectionError::ConnectionClosed(close) => {
                            if close.error_code == TransportErrorCode::PROTOCOL_VIOLATION {
                                ConnectionError::TransportError(TransportError::new(
                                    close.error_code,
                                    std::string::String::from_utf8_lossy(&close.reason[..])
                                        .to_string(),
                                ))
                            } else {
                                ConnectionError::ConnectionClosed(close)
                            }
                        }
                        e => e,
                    };
                    (Some(error), false)
                }
                InnerState::Handshake(_) | InnerState::Established => (None, false),
            };
            (error, self.is_local_close(), was_draining)
        };
        self.inner = InnerState::Drained { error, is_local };
        trace!("connection state: drained");
        was_draining
    }

    /// Moves to a draining state.
    ///
    /// Panics if the state is already draining or drained.
    pub(super) fn move_to_draining(&mut self, error: Option<ConnectionError>) {
        assert!(
            matches!(
                self.inner,
                InnerState::Handshake(_) | InnerState::Established | InnerState::Closed { .. }
            ),
            "invalid state transition {:?} -> draining",
            self.as_type()
        );
        let is_local = self.is_local_close();

        // If no error is provided and we were in the `Closed` state with an unread non-local
        // error, preserve that error so `take_error` can still surface it. Otherwise the
        // `ConnectionLost` event would never be emitted, breaking the invariant that a
        // drained noq-proto connection will always produce a `ConnectionLost` event.
        let error = error.or_else(|| {
            if let InnerState::Closed {
                ref remote_reason,
                error_read: false,
                is_local: false,
            } = self.inner
            {
                Some(remote_reason.clone().into())
            } else {
                None
            }
        });

        self.inner = InnerState::Draining { error, is_local };
        trace!("connection state: draining");
    }

    fn is_local_close(&self) -> bool {
        match self.inner {
            InnerState::Handshake(_) => false,
            InnerState::Established => false,
            InnerState::Closed { is_local, .. } => is_local,
            InnerState::Draining { is_local, .. } => is_local,
            InnerState::Drained { is_local, .. } => is_local,
        }
    }

    /// Enters the Closing connection state, due to changes in the [`Connection`] state.
    ///
    /// This is the closing state from
    /// <https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.1> due to the local side
    /// having initiated immediate close.
    ///
    /// Crucially, this is to be used when internal state changes result in initiating an
    /// immediate close. The resulting error will be surfaced as a [`ConnectionLost`] event
    /// in [`Connection::poll`].
    ///
    /// # Panics
    ///
    /// Panics if the state is later than established.
    ///
    /// [`Connection`]: super::Connection
    /// [`ConnectionLost`]: crate::Event::ConnectionLost
    /// [`Connection::poll`]: super::Connection::poll
    pub(super) fn move_to_closed<R: Into<CloseReason>>(&mut self, reason: R) {
        assert!(
            matches!(
                self.inner,
                InnerState::Handshake(_) | InnerState::Established | InnerState::Closed { .. }
            ),
            "invalid state transition {:?} -> closed",
            self.as_type()
        );
        let remote_reason = reason.into();
        let is_local = false;
        trace!(?remote_reason, ?is_local, "connection state: closed");
        self.inner = InnerState::Closed {
            error_read: false,
            remote_reason,
            is_local,
        };
    }

    /// Enters the Closing connection state, initiated by explicit API calls.
    ///
    /// This is the closing state from
    /// <https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.1> due to the local side
    /// having initiated immediate close.
    ///
    /// Crucially, this is to be used when immediate close is entered due to an API
    /// being called. It means the close will NOT surface as a [`ConnectionLost`] event in
    /// [`Connection::poll`].
    ///
    /// See [`Self::move_to_closed`] for when the internal state changes resulted in
    /// initiating an immediate close.
    ///
    /// # Panics
    ///
    /// Panics if the state is later than established.
    ///
    /// [`ConnectionLost`]: crate::Event::ConnectionLost
    /// [`Connection::poll`]: super::Connection::poll
    pub(super) fn move_to_closed_local<R: Into<CloseReason>>(&mut self, reason: R) {
        assert!(
            matches!(
                self.inner,
                InnerState::Handshake(_) | InnerState::Established | InnerState::Closed { .. }
            ),
            "invalid state transition {:?} -> closed (local)",
            self.as_type()
        );
        let remote_reason = reason.into();
        let is_local = true;
        trace!(?remote_reason, ?is_local, "connection state: closed");
        self.inner = InnerState::Closed {
            error_read: false,
            remote_reason,
            is_local,
        };
    }

    pub(super) fn is_handshake(&self) -> bool {
        matches!(self.inner, InnerState::Handshake(_))
    }

    pub(super) fn is_established(&self) -> bool {
        matches!(self.inner, InnerState::Established)
    }

    pub(super) fn is_closed(&self) -> bool {
        matches!(
            self.inner,
            InnerState::Closed { .. } | InnerState::Draining { .. } | InnerState::Drained { .. }
        )
    }

    pub(super) fn is_drained(&self) -> bool {
        matches!(self.inner, InnerState::Drained { .. })
    }

    pub(super) fn take_error(&mut self) -> Option<ConnectionError> {
        match &mut self.inner {
            InnerState::Draining { error, is_local } => {
                if !*is_local {
                    error.take()
                } else {
                    None
                }
            }
            InnerState::Drained { error, is_local } => {
                if !*is_local {
                    error.take()
                } else {
                    None
                }
            }
            InnerState::Closed {
                remote_reason,
                is_local: local_reason,
                error_read,
            } => {
                if *error_read {
                    None
                } else {
                    *error_read = true;
                    if *local_reason {
                        None
                    } else {
                        Some(remote_reason.clone().into())
                    }
                }
            }
            InnerState::Handshake(_) | InnerState::Established => None,
        }
    }

    pub(super) fn as_type(&self) -> StateType {
        match self.inner {
            InnerState::Handshake(_) => StateType::Handshake,
            InnerState::Established => StateType::Established,
            InnerState::Closed { .. } => StateType::Closed,
            InnerState::Draining { .. } => StateType::Draining,
            InnerState::Drained { .. } => StateType::Drained,
        }
    }
}

/// The state a [`Connection`] can be in.
///
/// [`Connection`]: super::Connection
#[derive(Debug, Clone)]
pub(super) enum StateType {
    /// Before the handshake is *confirmed*.
    Handshake,
    /// Once the handshake is *confirmed*.
    Established,
    /// The connection is closed, waiting for remote to confirm close.
    ///
    /// Specifically <https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.1>.
    ///
    /// So the side that initiates an immediate close will stay in this state while it is
    /// waiting for the remote to also send a CONNECTION_CLOSE. The side that receives a
    /// connection close will skip straight to [`StateType::Draining`].
    Closed,
    /// The connection is draining, giving time to gracefully discard any in-flight packets.
    ///
    /// Specifically <https://www.rfc-editor.org/rfc/rfc9000.html#section-10.2.2>.
    ///
    /// See [`StateType::Closed`] above for more details.
    Draining,
    /// The connection is drained, waiting for the application to drop us.
    ///
    /// This is a terminal state in which the connection does nothing and can never do
    /// anything again. Waiting for the application to drop the [`Connection`] struct.
    ///
    /// [`Connection`]: super::Connection
    Drained,
}

#[derive(Debug, Clone)]
pub(super) enum CloseReason {
    TransportError(TransportError),
    Connection(ConnectionClose),
    Application(ApplicationClose),
}

impl From<TransportError> for CloseReason {
    fn from(x: TransportError) -> Self {
        Self::TransportError(x)
    }
}
impl From<ConnectionClose> for CloseReason {
    fn from(x: ConnectionClose) -> Self {
        Self::Connection(x)
    }
}
impl From<ApplicationClose> for CloseReason {
    fn from(x: ApplicationClose) -> Self {
        Self::Application(x)
    }
}

impl From<Close> for CloseReason {
    fn from(value: Close) -> Self {
        match value {
            Close::Application(reason) => Self::Application(reason),
            Close::Connection(reason) => Self::Connection(reason),
        }
    }
}

impl From<CloseReason> for ConnectionError {
    fn from(value: CloseReason) -> Self {
        match value {
            CloseReason::TransportError(err) => Self::TransportError(err),
            CloseReason::Connection(reason) => Self::ConnectionClosed(reason),
            CloseReason::Application(reason) => Self::ApplicationClosed(reason),
        }
    }
}

impl From<CloseReason> for Close {
    fn from(value: CloseReason) -> Self {
        match value {
            CloseReason::TransportError(err) => Self::Connection(err.into()),
            CloseReason::Connection(reason) => Self::Connection(reason),
            CloseReason::Application(reason) => Self::Application(reason),
        }
    }
}

#[derive(Debug, Clone)]
enum InnerState {
    /// See [`StateType::Handshake`].
    Handshake(Handshake),
    /// See [`StateType::Established`].
    Established,
    /// See [`StateType::Closed`].
    Closed {
        /// The reason the remote closed the connection, or the reason we are sending to the remote.
        remote_reason: CloseReason,
        /// Set to true if we closed the connection locally.
        is_local: bool,
        /// Did we read this as error already?
        error_read: bool,
    },
    /// See [`StateType::Draining`].
    Draining {
        /// Why the connection was lost, if it has been.
        error: Option<ConnectionError>,
        /// Set to true if we closed the connection locally.
        is_local: bool,
    },
    /// See [`StateType::Drained`].
    /// Waiting for application to call close so we can dispose of the resources.
    Drained {
        /// Why the connection was lost, if it has been.
        error: Option<ConnectionError>,
        /// Set to true if we closed the connection locally.
        is_local: bool,
    },
}

#[allow(unreachable_pub)] // fuzzing only
#[derive(Debug, Clone)]
pub struct Handshake {
    /// Whether the remote CID has been set by the peer yet.
    ///
    /// Always set for servers.
    pub(super) remote_cid_set: bool,
    /// Stateless retry token received in the first Initial by a server.
    ///
    /// Must be present in every Initial. Always empty for clients.
    pub(super) expected_token: Bytes,
    /// First cryptographic message.
    ///
    /// Only set for clients.
    pub(super) client_hello: Option<Bytes>,
    /// Whether the server address is allowed to migrate.
    ///
    /// We allow the server to migrate during the handshake as long as we have not
    /// received an authenticated handshake packet: it can send a response from a
    /// different address than we sent the initial to.  This allows us to send the
    /// initial packet over multiple paths - by means of an IPv6 ULA address that copies
    /// the packets sent to it to multiple destinations - and accept one response.
    ///
    /// This is only ever set to true if for a client which hasn't yet received an
    /// authenticated handshake packet.  It is set back to false in
    /// [`super::Connection::on_packet_authenticated`].
    ///
    /// THIS IS NOT RFC 9000 COMPLIANT!  A server is not allowed to migrate addresses,
    /// other than using the preferred-address transport parameter.
    pub(super) allow_server_migration: bool,
}
