//! Eclipse attack mitigations.
//!
//! Eclipse attacks attempt to isolate a node by controlling all its peer connections.
//! This module implements several mitigations:
//!
//! - **Subnet diversity**: Limit peers from the same /16 subnet
//! - **Anchor connections**: Maintain persistent connections to trusted peers
//! - **Outbound ratio**: Ensure minimum outbound connections
//! - **Random rotation**: Periodically rotate a subset of peers
//! - **New peer trials**: Test new peers before fully trusting them

use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::time::{Duration, Instant};

use libp2p::PeerId;

/// Configuration for eclipse attack protection.
#[derive(Clone, Debug)]
pub struct EclipseConfig {
    /// Maximum peers from the same /16 subnet.
    pub max_peers_per_subnet: usize,
    /// Minimum outbound connections (must be maintained).
    pub min_outbound: usize,
    /// Number of anchor (persistent) connections.
    pub anchor_count: usize,
    /// How often to rotate peers (0 = never).
    pub rotation_interval: Duration,
    /// Percentage of peers to rotate each interval.
    pub rotation_percentage: f32,
    /// Trial period for new peers before full trust.
    pub trial_period: Duration,
    /// Maximum trial peers at once.
    pub max_trial_peers: usize,
}

impl Default for EclipseConfig {
    fn default() -> Self {
        EclipseConfig {
            max_peers_per_subnet: 2,
            min_outbound: 8,
            anchor_count: 4,
            rotation_interval: Duration::from_secs(3600), // 1 hour
            rotation_percentage: 0.1,                     // 10%
            trial_period: Duration::from_secs(300),       // 5 minutes
            max_trial_peers: 5,
        }
    }
}

/// Peer connection type.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PeerType {
    /// Outbound connection we initiated.
    Outbound,
    /// Inbound connection peer initiated.
    Inbound,
    /// Anchor peer (persistent, trusted).
    Anchor,
    /// Trial peer (new, not yet trusted).
    Trial,
}

/// Information about a connected peer for eclipse protection.
#[derive(Clone, Debug)]
pub struct PeerInfo {
    /// Peer ID.
    pub peer_id: PeerId,
    /// IP address.
    pub ip: Option<IpAddr>,
    /// Connection type.
    pub peer_type: PeerType,
    /// When the peer connected.
    pub connected_at: Instant,
    /// /16 subnet identifier.
    pub subnet: Option<u32>,
}

impl PeerInfo {
    /// Creates new peer info.
    pub fn new(peer_id: PeerId, ip: Option<IpAddr>, peer_type: PeerType) -> Self {
        let subnet = ip.and_then(|addr| Self::extract_subnet(&addr));
        PeerInfo {
            peer_id,
            ip,
            peer_type,
            connected_at: Instant::now(),
            subnet,
        }
    }

    /// Extracts /16 subnet identifier from IP.
    fn extract_subnet(ip: &IpAddr) -> Option<u32> {
        match ip {
            IpAddr::V4(addr) => {
                let octets = addr.octets();
                Some(((octets[0] as u32) << 8) | (octets[1] as u32))
            }
            IpAddr::V6(addr) => {
                // Use first 32 bits for IPv6 subnet grouping
                let segments = addr.segments();
                Some(((segments[0] as u32) << 16) | (segments[1] as u32))
            }
        }
    }

    /// Checks if peer is in trial period.
    pub fn is_trial(&self, trial_period: Duration) -> bool {
        self.peer_type == PeerType::Trial || self.connected_at.elapsed() < trial_period
    }
}

/// Eclipse protection manager.
pub struct EclipseProtection {
    /// Configuration.
    config: EclipseConfig,
    /// Connected peers.
    peers: HashMap<PeerId, PeerInfo>,
    /// Peers per subnet.
    subnet_counts: HashMap<u32, usize>,
    /// Anchor peer IDs (persistent connections).
    anchors: HashSet<PeerId>,
    /// Last rotation time.
    last_rotation: Instant,
    /// Denied peer IDs (temporarily rejected).
    denied: HashMap<PeerId, Instant>,
}

impl EclipseProtection {
    /// Creates a new eclipse protection manager.
    pub fn new(config: EclipseConfig) -> Self {
        EclipseProtection {
            config,
            peers: HashMap::new(),
            subnet_counts: HashMap::new(),
            anchors: HashSet::new(),
            last_rotation: Instant::now(),
            denied: HashMap::new(),
        }
    }

    /// Checks if a new connection should be allowed.
    ///
    /// Returns `Ok(())` if allowed, `Err(reason)` if denied.
    pub fn check_connection(
        &self,
        peer_id: &PeerId,
        ip: Option<IpAddr>,
        is_outbound: bool,
    ) -> Result<(), EclipseDenialReason> {
        // Check if peer is temporarily denied
        if let Some(denied_until) = self.denied.get(peer_id) {
            if denied_until.elapsed() < Duration::from_secs(300) {
                return Err(EclipseDenialReason::TemporarilyDenied);
            }
        }

        // Check subnet diversity for inbound connections
        if !is_outbound {
            if let Some(ip) = ip {
                if let Some(subnet) = PeerInfo::extract_subnet(&ip) {
                    let current_count = self.subnet_counts.get(&subnet).copied().unwrap_or(0);
                    if current_count >= self.config.max_peers_per_subnet {
                        return Err(EclipseDenialReason::SubnetLimitReached);
                    }
                }
            }
        }

        // Check trial peer limit
        let trial_count = self.count_trial_peers();
        if trial_count >= self.config.max_trial_peers && !self.anchors.contains(peer_id) {
            return Err(EclipseDenialReason::TooManyTrialPeers);
        }

        Ok(())
    }

    /// Registers a new peer connection.
    pub fn register_peer(&mut self, peer_id: PeerId, ip: Option<IpAddr>, is_outbound: bool) {
        let peer_type = if self.anchors.contains(&peer_id) {
            PeerType::Anchor
        } else if is_outbound {
            PeerType::Outbound
        } else {
            PeerType::Trial // Inbound connections start as trial
        };

        let info = PeerInfo::new(peer_id, ip, peer_type);

        // Update subnet count
        if let Some(subnet) = info.subnet {
            *self.subnet_counts.entry(subnet).or_insert(0) += 1;
        }

        self.peers.insert(peer_id, info);
    }

    /// Removes a peer from tracking.
    pub fn remove_peer(&mut self, peer_id: &PeerId) {
        if let Some(info) = self.peers.remove(peer_id) {
            if let Some(subnet) = info.subnet {
                if let Some(count) = self.subnet_counts.get_mut(&subnet) {
                    *count = count.saturating_sub(1);
                    if *count == 0 {
                        self.subnet_counts.remove(&subnet);
                    }
                }
            }
        }
    }

    /// Promotes a trial peer to regular status.
    pub fn promote_peer(&mut self, peer_id: &PeerId) {
        if let Some(info) = self.peers.get_mut(peer_id) {
            if info.peer_type == PeerType::Trial {
                info.peer_type = PeerType::Inbound;
            }
        }
    }

    /// Adds a peer to the anchor set.
    pub fn add_anchor(&mut self, peer_id: PeerId) {
        self.anchors.insert(peer_id);
        if let Some(info) = self.peers.get_mut(&peer_id) {
            info.peer_type = PeerType::Anchor;
        }
    }

    /// Removes a peer from the anchor set.
    pub fn remove_anchor(&mut self, peer_id: &PeerId) {
        self.anchors.remove(peer_id);
    }

    /// Gets peers that should be rotated.
    pub fn get_rotation_candidates(&mut self) -> Vec<PeerId> {
        if self.last_rotation.elapsed() < self.config.rotation_interval {
            return Vec::new();
        }

        self.last_rotation = Instant::now();

        // Only rotate non-anchor, non-outbound peers
        let candidates: Vec<_> = self
            .peers
            .iter()
            .filter(|(_, info)| {
                info.peer_type != PeerType::Anchor
                    && info.peer_type != PeerType::Outbound
                    && info.connected_at.elapsed() > self.config.trial_period
            })
            .map(|(id, _)| *id)
            .collect();

        let rotate_count =
            (candidates.len() as f32 * self.config.rotation_percentage).ceil() as usize;

        // Return random subset (simplified: just take first N)
        candidates.into_iter().take(rotate_count).collect()
    }

    /// Checks if we need more outbound connections.
    pub fn needs_outbound(&self) -> bool {
        let outbound_count = self
            .peers
            .values()
            .filter(|p| p.peer_type == PeerType::Outbound || p.peer_type == PeerType::Anchor)
            .count();

        outbound_count < self.config.min_outbound
    }

    /// Returns the number of outbound connections needed.
    pub fn outbound_deficit(&self) -> usize {
        let outbound_count = self
            .peers
            .values()
            .filter(|p| p.peer_type == PeerType::Outbound || p.peer_type == PeerType::Anchor)
            .count();

        self.config.min_outbound.saturating_sub(outbound_count)
    }

    /// Temporarily denies a peer.
    pub fn deny_peer(&mut self, peer_id: PeerId, _reason: &str) {
        self.denied.insert(peer_id, Instant::now());
    }

    /// Cleans up expired denials.
    pub fn cleanup_denials(&mut self) {
        let expiry = Duration::from_secs(300);
        self.denied.retain(|_, time| time.elapsed() < expiry);
    }

    /// Counts trial peers.
    fn count_trial_peers(&self) -> usize {
        self.peers
            .values()
            .filter(|p| p.is_trial(self.config.trial_period))
            .count()
    }

    /// Gets connection statistics.
    pub fn stats(&self) -> EclipseStats {
        let mut inbound = 0;
        let mut outbound = 0;
        let mut anchor = 0;
        let mut trial = 0;

        for info in self.peers.values() {
            match info.peer_type {
                PeerType::Inbound => inbound += 1,
                PeerType::Outbound => outbound += 1,
                PeerType::Anchor => anchor += 1,
                PeerType::Trial => trial += 1,
            }
        }

        EclipseStats {
            total_peers: self.peers.len(),
            inbound,
            outbound,
            anchor,
            trial,
            unique_subnets: self.subnet_counts.len(),
        }
    }
}

/// Reason for denying a connection.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EclipseDenialReason {
    /// Too many peers from the same subnet.
    SubnetLimitReached,
    /// Peer is temporarily denied.
    TemporarilyDenied,
    /// Too many trial peers.
    TooManyTrialPeers,
}

impl std::fmt::Display for EclipseDenialReason {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::SubnetLimitReached => write!(f, "subnet limit reached"),
            Self::TemporarilyDenied => write!(f, "temporarily denied"),
            Self::TooManyTrialPeers => write!(f, "too many trial peers"),
        }
    }
}

/// Eclipse protection statistics.
#[derive(Clone, Debug, Default)]
pub struct EclipseStats {
    /// Total connected peers.
    pub total_peers: usize,
    /// Inbound connections.
    pub inbound: usize,
    /// Outbound connections.
    pub outbound: usize,
    /// Anchor connections.
    pub anchor: usize,
    /// Trial connections.
    pub trial: usize,
    /// Unique subnets represented.
    pub unique_subnets: usize,
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::Ipv4Addr;

    fn random_peer() -> PeerId {
        PeerId::random()
    }

    #[test]
    fn test_subnet_extraction() {
        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
        let subnet = PeerInfo::extract_subnet(&ip).unwrap();
        // 192.168.x.x -> (192 << 8) | 168 = 49320
        assert_eq!(subnet, (192 << 8) | 168);

        let ip2 = IpAddr::V4(Ipv4Addr::new(192, 168, 2, 100));
        let subnet2 = PeerInfo::extract_subnet(&ip2).unwrap();
        assert_eq!(subnet, subnet2); // Same /16 subnet
    }

    #[test]
    fn test_subnet_diversity() {
        let config = EclipseConfig {
            max_peers_per_subnet: 2,
            ..Default::default()
        };
        let mut protection = EclipseProtection::new(config);

        let ip = Some(IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)));

        // First two should be allowed
        let peer1 = random_peer();
        assert!(protection.check_connection(&peer1, ip, false).is_ok());
        protection.register_peer(peer1, ip, false);

        let peer2 = random_peer();
        assert!(protection.check_connection(&peer2, ip, false).is_ok());
        protection.register_peer(peer2, ip, false);

        // Third should be denied (same subnet)
        let peer3 = random_peer();
        assert_eq!(
            protection.check_connection(&peer3, ip, false),
            Err(EclipseDenialReason::SubnetLimitReached)
        );

        // Different subnet should be allowed
        let different_ip = Some(IpAddr::V4(Ipv4Addr::new(10, 1, 1, 1)));
        assert!(protection
            .check_connection(&peer3, different_ip, false)
            .is_ok());
    }

    #[test]
    fn test_outbound_tracking() {
        let config = EclipseConfig {
            min_outbound: 4,
            ..Default::default()
        };
        let mut protection = EclipseProtection::new(config);

        assert!(protection.needs_outbound());
        assert_eq!(protection.outbound_deficit(), 4);

        // Add outbound peers
        for _ in 0..4 {
            let peer = random_peer();
            protection.register_peer(peer, None, true);
        }

        assert!(!protection.needs_outbound());
        assert_eq!(protection.outbound_deficit(), 0);
    }

    #[test]
    fn test_anchor_peers() {
        let config = EclipseConfig::default();
        let mut protection = EclipseProtection::new(config);

        let anchor = random_peer();
        protection.add_anchor(anchor);
        protection.register_peer(anchor, None, true);

        let stats = protection.stats();
        assert_eq!(stats.anchor, 1);
    }

    #[test]
    fn test_peer_denial() {
        let config = EclipseConfig::default();
        let mut protection = EclipseProtection::new(config);

        let peer = random_peer();
        protection.deny_peer(peer, "bad behavior");

        assert_eq!(
            protection.check_connection(&peer, None, false),
            Err(EclipseDenialReason::TemporarilyDenied)
        );
    }
}
