//! Peer Management
//!
//! Two-phase peer lifecycle:
//! 1. **PeerConnection** - Handshake phase, before identity is verified
//! 2. **ActivePeer** - Authenticated phase, after successful Noise handshake
//!
//! The PeerSlot enum represents either phase, enabling unified storage
//! while maintaining type safety for phase-specific operations.

mod active;
mod connection;

pub use active::{ActivePeer, ConnectivityState};
pub use connection::{HandshakeState, PeerConnection};

use crate::NodeAddr;
use crate::transport::LinkId;
use std::fmt;
use thiserror::Error;

// ============================================================================
// Errors
// ============================================================================

/// Errors related to peer operations.
#[derive(Debug, Error)]
pub enum PeerError {
    #[error("peer not authenticated")]
    NotAuthenticated,

    #[error("peer not found: {0:?}")]
    NotFound(NodeAddr),

    #[error("connection not found: {0}")]
    ConnectionNotFound(LinkId),

    #[error("peer already exists: {0:?}")]
    AlreadyExists(NodeAddr),

    #[error("handshake failed: {0}")]
    HandshakeFailed(String),

    #[error("handshake timeout")]
    HandshakeTimeout,

    #[error("identity mismatch: expected {expected:?}, got {actual:?}")]
    IdentityMismatch {
        expected: NodeAddr,
        actual: NodeAddr,
    },

    #[error("peer disconnected")]
    Disconnected,

    #[error("max connections exceeded: {max}")]
    MaxConnectionsExceeded { max: usize },

    #[error("max peers exceeded: {max}")]
    MaxPeersExceeded { max: usize },
}

// ============================================================================
// Cross-Connection Handling
// ============================================================================

/// Result of attempting to promote a connection to active peer.
///
/// When a handshake completes, we may discover that we already have a
/// connection to this peer (cross-connection). The tie-breaker rule
/// determines which connection survives.
///
/// Note: Returns NodeAddr instead of ActivePeer because ActivePeer cannot
/// be cloned (it contains NoiseSession which has cryptographic state).
/// Callers can look up the peer from the peers map using the NodeAddr.
#[derive(Debug, Clone, Copy)]
pub enum PromotionResult {
    /// New peer created successfully.
    Promoted(NodeAddr),

    /// Cross-connection detected. This connection lost the tie-breaker
    /// and should be closed.
    CrossConnectionLost {
        /// The link that won (existing connection).
        winner_link_id: LinkId,
    },

    /// Cross-connection detected. This connection won the tie-breaker.
    /// The existing connection was replaced.
    CrossConnectionWon {
        /// The link that lost (previous connection, now closed).
        loser_link_id: LinkId,
        /// The node ID of the peer.
        node_addr: NodeAddr,
    },
}

impl PromotionResult {
    /// Get the node ID if promotion succeeded.
    pub fn node_addr(&self) -> Option<NodeAddr> {
        match self {
            PromotionResult::Promoted(node_addr) => Some(*node_addr),
            PromotionResult::CrossConnectionWon { node_addr, .. } => Some(*node_addr),
            PromotionResult::CrossConnectionLost { .. } => None,
        }
    }

    /// Check if this connection should be closed.
    pub fn should_close_this_connection(&self) -> bool {
        matches!(self, PromotionResult::CrossConnectionLost { .. })
    }

    /// Get the link that should be closed, if any.
    pub fn link_to_close(&self) -> Option<LinkId> {
        match self {
            PromotionResult::CrossConnectionLost { .. } => None, // Caller's link
            PromotionResult::CrossConnectionWon { loser_link_id, .. } => Some(*loser_link_id),
            PromotionResult::Promoted(_) => None,
        }
    }
}

/// Determine winner of cross-connection tie-breaker.
///
/// Rule: The node with the smaller node_addr prefers its OUTBOUND connection.
/// This is deterministic and symmetric: both nodes will reach the same conclusion.
///
/// # Arguments
/// * `our_node_addr` - Our node's ID
/// * `their_node_addr` - The peer's node ID
/// * `this_is_outbound` - Whether the connection being evaluated is our outbound
///
/// # Returns
/// `true` if this connection should win (survive), `false` if it should close.
pub fn cross_connection_winner(
    our_node_addr: &NodeAddr,
    their_node_addr: &NodeAddr,
    this_is_outbound: bool,
) -> bool {
    let we_are_smaller = our_node_addr < their_node_addr;

    // Smaller node's outbound wins
    // If we're smaller: our outbound wins, our inbound loses
    // If they're smaller: our outbound loses, our inbound wins
    if we_are_smaller {
        this_is_outbound
    } else {
        !this_is_outbound
    }
}

// ============================================================================
// PeerSlot
// ============================================================================

/// A slot in the peer table, representing either connection or active phase.
#[derive(Debug)]
pub enum PeerSlot {
    /// Connection in handshake phase.
    Connecting(Box<PeerConnection>),
    /// Authenticated peer.
    Active(Box<ActivePeer>),
}

impl PeerSlot {
    /// Create a new connecting slot (outbound).
    pub fn outbound(conn: PeerConnection) -> Self {
        PeerSlot::Connecting(Box::new(conn))
    }

    /// Create a new connecting slot (inbound).
    pub fn inbound(conn: PeerConnection) -> Self {
        PeerSlot::Connecting(Box::new(conn))
    }

    /// Create a new active slot.
    pub fn active(peer: ActivePeer) -> Self {
        PeerSlot::Active(Box::new(peer))
    }

    /// Check if this is a connecting slot.
    pub fn is_connecting(&self) -> bool {
        matches!(self, PeerSlot::Connecting(_))
    }

    /// Check if this is an active slot.
    pub fn is_active(&self) -> bool {
        matches!(self, PeerSlot::Active(_))
    }

    /// Get the link ID for this slot.
    pub fn link_id(&self) -> LinkId {
        match self {
            PeerSlot::Connecting(conn) => conn.link_id(),
            PeerSlot::Active(peer) => peer.link_id(),
        }
    }

    /// Get as connection reference, if connecting.
    pub fn as_connection(&self) -> Option<&PeerConnection> {
        match self {
            PeerSlot::Connecting(conn) => Some(conn),
            PeerSlot::Active(_) => None,
        }
    }

    /// Get as mutable connection reference, if connecting.
    pub fn as_connection_mut(&mut self) -> Option<&mut PeerConnection> {
        match self {
            PeerSlot::Connecting(conn) => Some(conn),
            PeerSlot::Active(_) => None,
        }
    }

    /// Get as active peer reference, if active.
    pub fn as_active(&self) -> Option<&ActivePeer> {
        match self {
            PeerSlot::Active(peer) => Some(peer),
            PeerSlot::Connecting(_) => None,
        }
    }

    /// Get as mutable active peer reference, if active.
    pub fn as_active_mut(&mut self) -> Option<&mut ActivePeer> {
        match self {
            PeerSlot::Active(peer) => Some(peer),
            PeerSlot::Connecting(_) => None,
        }
    }

    /// Get the known node_addr, if any.
    ///
    /// For connections, this is the expected identity (may be None for inbound).
    /// For active peers, this is always known.
    pub fn node_addr(&self) -> Option<&NodeAddr> {
        match self {
            PeerSlot::Connecting(conn) => conn.expected_identity().map(|id| id.node_addr()),
            PeerSlot::Active(peer) => Some(peer.node_addr()),
        }
    }
}

impl fmt::Display for PeerSlot {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PeerSlot::Connecting(conn) => {
                write!(
                    f,
                    "connecting(link={}, state={})",
                    conn.link_id(),
                    conn.handshake_state()
                )
            }
            PeerSlot::Active(peer) => {
                write!(
                    f,
                    "active(node={:?}, link={})",
                    peer.node_addr(),
                    peer.link_id()
                )
            }
        }
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::transport::LinkId;
    use crate::{Identity, PeerIdentity};

    fn make_node_addr(val: u8) -> NodeAddr {
        let mut bytes = [0u8; 16];
        bytes[0] = val;
        NodeAddr::from_bytes(bytes)
    }

    fn make_peer_identity() -> PeerIdentity {
        let identity = Identity::generate();
        PeerIdentity::from_pubkey(identity.pubkey())
    }

    #[test]
    fn test_cross_connection_smaller_node_wins_outbound() {
        let node_a = make_node_addr(1); // smaller
        let node_b = make_node_addr(2); // larger

        // Node A's perspective
        assert!(cross_connection_winner(&node_a, &node_b, true)); // A's outbound wins
        assert!(!cross_connection_winner(&node_a, &node_b, false)); // A's inbound loses

        // Node B's perspective
        assert!(!cross_connection_winner(&node_b, &node_a, true)); // B's outbound loses
        assert!(cross_connection_winner(&node_b, &node_a, false)); // B's inbound wins
    }

    #[test]
    fn test_cross_connection_symmetric() {
        let node_a = make_node_addr(1);
        let node_b = make_node_addr(2);

        // A's outbound = B's inbound
        let a_outbound_wins = cross_connection_winner(&node_a, &node_b, true);
        let b_inbound_wins = cross_connection_winner(&node_b, &node_a, false);
        assert_eq!(a_outbound_wins, b_inbound_wins);

        // A's inbound = B's outbound
        let a_inbound_wins = cross_connection_winner(&node_a, &node_b, false);
        let b_outbound_wins = cross_connection_winner(&node_b, &node_a, true);
        assert_eq!(a_inbound_wins, b_outbound_wins);

        // Exactly one survives
        assert!(a_outbound_wins != a_inbound_wins);
    }

    #[test]
    fn test_peer_slot_connecting() {
        let identity = make_peer_identity();
        let conn = PeerConnection::outbound(LinkId::new(1), identity, 1000);
        let slot = PeerSlot::Connecting(Box::new(conn));

        assert!(slot.is_connecting());
        assert!(!slot.is_active());
        assert!(slot.as_connection().is_some());
        assert!(slot.as_active().is_none());
        assert_eq!(slot.link_id(), LinkId::new(1));
    }

    #[test]
    fn test_peer_slot_active() {
        let identity = make_peer_identity();
        let peer = ActivePeer::new(identity, LinkId::new(2), 2000);
        let slot = PeerSlot::Active(Box::new(peer));

        assert!(!slot.is_connecting());
        assert!(slot.is_active());
        assert!(slot.as_connection().is_none());
        assert!(slot.as_active().is_some());
        assert_eq!(slot.link_id(), LinkId::new(2));
    }

    #[test]
    fn test_promotion_result_promoted() {
        let identity = make_peer_identity();
        let node_addr = *identity.node_addr();
        let result = PromotionResult::Promoted(node_addr);

        assert!(result.node_addr().is_some());
        assert_eq!(result.node_addr(), Some(node_addr));
        assert!(!result.should_close_this_connection());
        assert!(result.link_to_close().is_none());
    }

    #[test]
    fn test_promotion_result_cross_lost() {
        let result = PromotionResult::CrossConnectionLost {
            winner_link_id: LinkId::new(1),
        };

        assert!(result.node_addr().is_none());
        assert!(result.should_close_this_connection());
        assert!(result.link_to_close().is_none()); // Caller closes their own
    }

    #[test]
    fn test_promotion_result_cross_won() {
        let identity = make_peer_identity();
        let node_addr = *identity.node_addr();
        let result = PromotionResult::CrossConnectionWon {
            loser_link_id: LinkId::new(1),
            node_addr,
        };

        assert!(result.node_addr().is_some());
        assert_eq!(result.node_addr(), Some(node_addr));
        assert!(!result.should_close_this_connection());
        assert_eq!(result.link_to_close(), Some(LinkId::new(1)));
    }

    #[test]
    fn test_peer_slot_node_addr() {
        // Outbound connection knows expected identity
        let identity = make_peer_identity();
        let expected_node_addr = *identity.node_addr();
        let conn = PeerConnection::outbound(LinkId::new(1), identity, 1000);
        let slot = PeerSlot::Connecting(Box::new(conn));
        assert_eq!(slot.node_addr(), Some(&expected_node_addr));

        // Inbound connection doesn't know identity yet
        let conn_inbound = PeerConnection::inbound(LinkId::new(2), 2000);
        let slot_inbound = PeerSlot::Connecting(Box::new(conn_inbound));
        assert!(slot_inbound.node_addr().is_none());

        // Active peer always knows identity
        let identity2 = make_peer_identity();
        let active_node_addr = *identity2.node_addr();
        let peer = ActivePeer::new(identity2, LinkId::new(3), 3000);
        let slot_active = PeerSlot::Active(Box::new(peer));
        assert_eq!(slot_active.node_addr(), Some(&active_node_addr));
    }
}
