use std::fmt;
use std::sync::Arc;
use std::time::Duration;

use crate::{
    context::SessionContext,
    error::{DialerErrorKind, ListenErrorKind, ProtocolHandleErrorKind},
    multiaddr::Multiaddr,
    service::{future_task::BoxedFutureTask, TargetProtocol, TargetSession},
    ProtocolId, SessionId,
};
use bytes::Bytes;

/// Error generated by the Service
#[derive(Debug)]
pub enum ServiceError {
    /// When dial remote error
    DialerError {
        /// Remote address
        address: Multiaddr,
        /// error
        error: DialerErrorKind,
    },
    /// When listen error
    ListenError {
        /// Listen address
        address: Multiaddr,
        /// error
        error: ListenErrorKind,
    },
    /// Protocol select fail
    ProtocolSelectError {
        /// Protocol name, if none, timeout or other net problem,
        /// if Some, don't support this proto
        proto_name: Option<String>,
        /// Session context
        session_context: Arc<SessionContext>,
    },
    /// Protocol error during interaction
    ProtocolError {
        /// Session id
        id: SessionId,
        /// Protocol id
        proto_id: ProtocolId,
        /// Codec error
        error: std::io::Error,
    },
    /// After initializing the connection, the session does not open any protocol,
    /// suspected fd attack
    SessionTimeout {
        /// Session context
        session_context: Arc<SessionContext>,
    },
    /// Multiplex protocol error
    MuxerError {
        /// Session context
        session_context: Arc<SessionContext>,
        /// error, such as `InvalidData`
        error: std::io::Error,
    },
    /// Protocol handle error, will cause memory leaks/abnormal CPU usage
    /// tentacle will close after this error output
    ProtocolHandleError {
        /// Protocol id
        proto_id: ProtocolId,
        /// error
        error: ProtocolHandleErrorKind,
    },
    /// Session blocked, can't send message, if the task is too heavy in a short time.
    /// such as too many data cache on this session and can't send to remote,
    /// it may cause oom, so this session will be kill by tentacle
    ///
    /// Judging condition: unsent message size > send buffer size set by the user, default 24m
    SessionBlocked {
        /// Session context
        session_context: Arc<SessionContext>,
    },
}

/// Event generated by the Service
#[derive(Debug)]
pub enum ServiceEvent {
    /// A session close
    SessionClose {
        /// Session context
        session_context: Arc<SessionContext>,
    },
    /// A session open
    SessionOpen {
        /// Session context
        session_context: Arc<SessionContext>,
    },
    /// Listen close
    ListenClose {
        /// Listen address
        address: Multiaddr,
    },
    /// Listen start
    ListenStarted {
        /// Listen address
        address: Multiaddr,
    },
}

/// Event generated by all protocol
#[derive(Debug)]
pub enum ProtocolEvent {
    /// Protocol open event
    Connected {
        /// session context
        session_context: Arc<SessionContext>,
        /// Protocol id
        proto_id: ProtocolId,
        /// Protocol version
        version: String,
    },
    /// Received protocol data
    Received {
        /// Session context
        session_context: Arc<SessionContext>,
        /// Protocol id
        proto_id: ProtocolId,
        /// Protocol version
        data: bytes::Bytes,
    },
    /// Protocol close event
    Disconnected {
        /// Protocol id
        proto_id: ProtocolId,
        /// session context
        session_context: Arc<SessionContext>,
    },
}

/// Task received by the Service.
///
/// An instruction that the outside world can send to the service
pub(crate) enum ServiceTask {
    /// Send protocol data task
    ProtocolMessage {
        /// Specify which sessions to send to,
        /// None means broadcast
        target: TargetSession,
        /// protocol id
        proto_id: ProtocolId,
        /// data
        data: Bytes,
    },
    /// Open specify protocol
    ProtocolOpen {
        /// Session id
        session_id: SessionId,
        /// protocol id
        target: TargetProtocol,
    },
    /// Close specify protocol
    ProtocolClose {
        /// Session id
        session_id: SessionId,
        /// protocol id
        proto_id: ProtocolId,
    },
    /// Set service notify task
    SetProtocolNotify {
        /// Protocol id
        proto_id: ProtocolId,
        /// Timer interval
        interval: Duration,
        /// The timer token
        token: u64,
    },
    /// Remove serivce notify task
    RemoveProtocolNotify {
        /// Protocol id
        proto_id: ProtocolId,
        /// The timer token
        token: u64,
    },
    /// Set service notify task
    SetProtocolSessionNotify {
        /// Session id
        session_id: SessionId,
        /// Protocol id
        proto_id: ProtocolId,
        /// Timer interval
        interval: Duration,
        /// The timer token
        token: u64,
    },
    /// Remove serivce notify task
    RemoveProtocolSessionNotify {
        /// Session id
        session_id: SessionId,
        /// Protocol id
        proto_id: ProtocolId,
        /// The timer token
        token: u64,
    },
    /// Future task
    FutureTask {
        /// Future
        task: BoxedFutureTask,
    },
    /// Disconnect task
    Disconnect {
        /// Session id
        session_id: SessionId,
    },
    /// Dial task
    Dial {
        /// Remote address
        address: Multiaddr,
        /// Dial protocols
        target: TargetProtocol,
    },
    /// Listen task
    Listen {
        /// Listen address
        address: Multiaddr,
    },
    /// Shutdown service
    Shutdown(bool),
}

impl fmt::Debug for ServiceTask {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use self::ServiceTask::*;

        match self {
            ProtocolMessage {
                target,
                proto_id,
                data,
            } => write!(
                f,
                "id: {:?}, proto_id: {}, message: {:?}",
                target, proto_id, data
            ),
            SetProtocolNotify {
                proto_id, token, ..
            } => write!(f, "set protocol({}) notify({})", proto_id, token),
            RemoveProtocolNotify { proto_id, token } => {
                write!(f, "remove protocol({}) notify({})", proto_id, token)
            }
            SetProtocolSessionNotify {
                session_id,
                proto_id,
                token,
                ..
            } => write!(
                f,
                "set protocol({}) session({}) notify({})",
                proto_id, session_id, token
            ),
            RemoveProtocolSessionNotify {
                session_id,
                proto_id,
                token,
            } => write!(
                f,
                "remove protocol({}) session({}) notify({})",
                proto_id, session_id, token
            ),
            FutureTask { .. } => write!(f, "Future task"),
            Disconnect { session_id } => write!(f, "Disconnect session [{}]", session_id),
            Dial { address, .. } => write!(f, "Dial address: {}", address),
            Listen { address } => write!(f, "Listen address: {}", address),
            ProtocolOpen { session_id, target } => {
                write!(f, "Open session [{}] proto [{:?}]", session_id, target)
            }
            ProtocolClose {
                session_id,
                proto_id,
            } => write!(f, "Close session [{}] proto [{}]", session_id, proto_id),
            Shutdown(_) => write!(f, "Try close service"),
        }
    }
}
