use std::task::{Context, Poll};

use futures::StreamExt;
use libp2p::core::connection::ConnectionId;
use libp2p::core::ConnectedPoint;
use libp2p::swarm::handler::DummyConnectionHandler;
use libp2p::swarm::{
    ConnectionHandler, DialError, NetworkBehaviour, NetworkBehaviourAction, PollParameters,
};
use libp2p::{Multiaddr, PeerId};

use crate::metrics;
use crate::rpc::GoodbyeReason;

use super::peerdb::BanResult;
use super::{PeerManager, PeerManagerEvent, ReportSource};

impl NetworkBehaviour for PeerManager {
    type ConnectionHandler = DummyConnectionHandler;

    type OutEvent = PeerManagerEvent;

    /* Required trait members */

    fn new_handler(&mut self) -> Self::ConnectionHandler {
        DummyConnectionHandler::default()
    }

    fn inject_event(
        &mut self,
        _: PeerId,
        _: ConnectionId,
        _: <DummyConnectionHandler as ConnectionHandler>::OutEvent,
    ) {
        unreachable!("Dummy handler does not emit events")
    }

    fn poll(
        &mut self,
        cx: &mut Context<'_>,
        _params: &mut impl PollParameters,
    ) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ConnectionHandler>> {
        // perform the heartbeat when necessary
        while self.heartbeat.poll_tick(cx).is_ready() {
            self.heartbeat();
        }

        // poll the timeouts for pings and status'
        loop {
            match self.inbound_ping_peers.poll_next_unpin(cx) {
                Poll::Ready(Some(Ok(peer_id))) => {
                    self.inbound_ping_peers.insert(peer_id);
                    self.events.push(PeerManagerEvent::Ping(peer_id));
                }
                Poll::Ready(Some(Err(e))) => {
                    error!(error = %e.to_string(), "Failed to check for inbound peers to ping")
                }
                Poll::Ready(None) | Poll::Pending => break,
            }
        }

        loop {
            match self.outbound_ping_peers.poll_next_unpin(cx) {
                Poll::Ready(Some(Ok(peer_id))) => {
                    self.outbound_ping_peers.insert(peer_id);
                    self.events.push(PeerManagerEvent::Ping(peer_id));
                }
                Poll::Ready(Some(Err(e))) => {
                    error!(error = %e.to_string(), "Failed to check for outbound peers to ping")
                }
                Poll::Ready(None) | Poll::Pending => break,
            }
        }

        loop {
            match self.status_peers.poll_next_unpin(cx) {
                Poll::Ready(Some(Ok(peer_id))) => {
                    self.status_peers.insert(peer_id);
                    self.events.push(PeerManagerEvent::Status(peer_id))
                }
                Poll::Ready(Some(Err(e))) => {
                    error!(error = %e.to_string(), "Failed to check for peers to ping")
                }
                Poll::Ready(None) | Poll::Pending => break,
            }
        }

        if !self.events.is_empty() {
            return Poll::Ready(NetworkBehaviourAction::GenerateEvent(self.events.remove(0)));
        } else {
            self.events.shrink_to_fit();
        }

        Poll::Pending
    }

    /* Overwritten trait members */

    fn inject_connection_established(
        &mut self,
        peer_id: &PeerId,
        _connection_id: &ConnectionId,
        endpoint: &ConnectedPoint,
        _failed_addresses: Option<&Vec<Multiaddr>>,
        other_established: usize,
    ) {
        debug!(%peer_id, connection = ?endpoint.to_endpoint(), "Connection established");
        if other_established == 0 {
            // self.events.push(PeerManagerEvent::MetaData(*peer_id));
        }

        // Check NAT if metrics are enabled
        if self.network_globals.local_enr.read().udp().is_some() {
            metrics::check_nat();
        }

        // Check to make sure the peer is not supposed to be banned
        match self.ban_status(peer_id) {
            // TODO: directly emit the ban event?
            BanResult::BadScore => {
                // This is a faulty state
                error!(%peer_id, "Connected to a banned peer, re-banning");
                // Reban the peer
                self.goodbye_peer(peer_id, GoodbyeReason::Banned, ReportSource::PeerManager);
                return;
            }
            BanResult::BannedIp(ip_addr) => {
                // A good peer has connected to us via a banned IP address. We ban the peer and
                // prevent future connections.
                debug!(%peer_id, banned_ip = %ip_addr, "Peer connected via banned IP. Banning");
                self.goodbye_peer(peer_id, GoodbyeReason::BannedIP, ReportSource::PeerManager);
                return;
            }
            BanResult::NotBanned => {}
        }

        // Count dialing peers in the limit if the peer dialied us.
        let count_dialing = endpoint.is_listener();
        // Check the connection limits
        if self.peer_limit_reached(count_dialing)
            && self
                .network_globals
                .peers
                .read()
                .peer_info(peer_id)
                .map_or(true, |peer| !peer.has_future_duty())
        {
            // Gracefully disconnect the peer.
            self.disconnect_peer(*peer_id, GoodbyeReason::TooManyPeers);
            return;
        }

        // NOTE: We don't register peers that we are disconnecting immediately. The network service
        // does not need to know about these peers.
        match endpoint {
            ConnectedPoint::Listener { send_back_addr, .. } => {
                self.inject_connect_ingoing(peer_id, send_back_addr.clone(), None);
                self.events
                    .push(PeerManagerEvent::PeerConnectedIncoming(*peer_id));
            }
            ConnectedPoint::Dialer { address, .. } => {
                self.inject_connect_outgoing(peer_id, address.clone(), None);
                self.events
                    .push(PeerManagerEvent::PeerConnectedOutgoing(*peer_id));
            }
        }

        // increment prometheus metrics
        self.update_connected_peer_metrics();
        metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT);
    }
    fn inject_connection_closed(
        &mut self,
        peer_id: &PeerId,
        _: &ConnectionId,
        _: &ConnectedPoint,
        _: DummyConnectionHandler,
        remaining_established: usize,
    ) {
        if remaining_established > 0 {
            return;
        }

        // There are no more connections
        if self
            .network_globals
            .peers
            .read()
            .is_connected_or_disconnecting(peer_id)
        {
            // We are disconnecting the peer or the peer has already been connected.
            // Both these cases, the peer has been previously registered by the peer manager and
            // potentially the application layer.
            // Inform the application.
            self.events
                .push(PeerManagerEvent::PeerDisconnected(*peer_id));
            debug!(%peer_id, "Peer disconnected");
        }

        // NOTE: It may be the case that a rejected node, due to too many peers is disconnected
        // here and the peer manager has no knowledge of its connection. We insert it here for
        // reference so that peer manager can track this peer.
        self.inject_disconnect(peer_id);

        // Update the prometheus metrics
        self.update_connected_peer_metrics();
        metrics::inc_counter(&metrics::PEER_DISCONNECT_EVENT_COUNT);
    }

    fn inject_address_change(
        &mut self,
        _peer_id: &PeerId,
        _connection_id: &ConnectionId,
        old: &ConnectedPoint,
        new: &ConnectedPoint,
    ) {
        debug_assert!(
            matches!(
                (old, new),
                (
                    // inbound remains inbound
                    ConnectedPoint::Listener { .. },
                    ConnectedPoint::Listener { .. }
                ) | (
                    // outbound remains outbound
                    ConnectedPoint::Dialer { .. },
                    ConnectedPoint::Dialer { .. }
                )
            ),
            "A peer has changed between inbound and outbound"
        )
    }

    /// A dial attempt has failed.
    ///
    /// NOTE: It can be the case that we are dialing a peer and during the dialing process the peer
    /// connects and the dial attempt later fails. To handle this, we only update the peer_db if
    /// the peer is not already connected.
    fn inject_dial_failure(
        &mut self,
        peer_id: Option<PeerId>,
        _handler: DummyConnectionHandler,
        _error: &DialError,
    ) {
        if let Some(peer_id) = peer_id {
            if !self.network_globals.peers.read().is_connected(&peer_id) {
                self.inject_disconnect(&peer_id);
            }
        }
    }
}
