//! Peer reputation and banning system for Synor network.
//!
//! This module provides a comprehensive reputation system to track peer behavior
//! and automatically ban misbehaving peers. The system tracks:
//! - Reputation scores (0-100)
//! - Violation history
//! - Ban status and expiry
//! - Gradual score recovery over time
//!
//! # Score Mechanics
//!
//! - New peers start with a score of 50
//! - Successful interactions increase score (up to 100)
//! - Violations decrease score based on severity
//! - Peers are auto-banned when score drops below 20
//! - Scores gradually recover over time when peer behaves well

use hashbrown::HashMap;
use libp2p::PeerId;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};

/// Types of violations that affect peer reputation.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ViolationType {
    /// Peer sent an invalid or malformed message.
    InvalidMessage,
    /// Peer responded too slowly or timed out.
    SlowResponse,
    /// Peer is sending too many requests (spam).
    Spam,
    /// Peer violated the protocol rules.
    ProtocolViolation,
    /// Peer sent duplicate data.
    DuplicateData,
    /// Peer provided incorrect data (e.g., bad block).
    InvalidData,
    /// Peer disconnected unexpectedly.
    UnexpectedDisconnect,
    /// Peer failed to respond to a request.
    NoResponse,
}

impl ViolationType {
    /// Returns the penalty for this violation type.
    pub fn penalty(&self) -> u8 {
        match self {
            ViolationType::InvalidMessage => 15,
            ViolationType::SlowResponse => 5,
            ViolationType::Spam => 20,
            ViolationType::ProtocolViolation => 25,
            ViolationType::DuplicateData => 3,
            ViolationType::InvalidData => 30,
            ViolationType::UnexpectedDisconnect => 5,
            ViolationType::NoResponse => 10,
        }
    }

    /// Returns the description of this violation type.
    pub fn description(&self) -> &'static str {
        match self {
            ViolationType::InvalidMessage => "sent invalid or malformed message",
            ViolationType::SlowResponse => "responded too slowly",
            ViolationType::Spam => "sending excessive requests",
            ViolationType::ProtocolViolation => "violated protocol rules",
            ViolationType::DuplicateData => "sent duplicate data",
            ViolationType::InvalidData => "provided invalid data",
            ViolationType::UnexpectedDisconnect => "disconnected unexpectedly",
            ViolationType::NoResponse => "failed to respond",
        }
    }
}

/// A record of a single violation.
#[derive(Clone, Debug)]
pub struct ViolationRecord {
    /// Type of violation.
    pub violation_type: ViolationType,
    /// When the violation occurred.
    pub timestamp: Instant,
    /// Optional context about the violation.
    pub context: Option<String>,
}

impl ViolationRecord {
    /// Creates a new violation record.
    pub fn new(violation_type: ViolationType) -> Self {
        ViolationRecord {
            violation_type,
            timestamp: Instant::now(),
            context: None,
        }
    }

    /// Creates a new violation record with context.
    pub fn with_context(violation_type: ViolationType, context: String) -> Self {
        ViolationRecord {
            violation_type,
            timestamp: Instant::now(),
            context: Some(context),
        }
    }
}

/// Reputation tracking for a single peer.
#[derive(Clone, Debug)]
pub struct PeerReputation {
    /// Peer identifier.
    pub peer_id: PeerId,
    /// Current reputation score (0-100).
    pub score: u8,
    /// When the peer was last seen active.
    pub last_seen: Instant,
    /// History of violations.
    pub violations: Vec<ViolationRecord>,
    /// When the ban expires (None if not banned).
    pub ban_until: Option<Instant>,
    /// Total successful interactions.
    pub successes: u64,
    /// Total failed interactions.
    pub failures: u64,
    /// Last time the score was recovered.
    last_recovery: Instant,
}

impl PeerReputation {
    /// Initial reputation score for new peers.
    pub const INITIAL_SCORE: u8 = 50;
    /// Maximum reputation score.
    pub const MAX_SCORE: u8 = 100;
    /// Minimum reputation score (below this, peer is banned).
    pub const BAN_THRESHOLD: u8 = 20;
    /// Score increment for successful interactions.
    pub const SUCCESS_BONUS: u8 = 1;
    /// How often scores can recover (in seconds).
    pub const RECOVERY_INTERVAL_SECS: u64 = 300; // 5 minutes
    /// How much score recovers each interval.
    pub const RECOVERY_AMOUNT: u8 = 2;

    /// Creates a new peer reputation tracker.
    pub fn new(peer_id: PeerId) -> Self {
        let now = Instant::now();
        PeerReputation {
            peer_id,
            score: Self::INITIAL_SCORE,
            last_seen: now,
            violations: Vec::new(),
            ban_until: None,
            successes: 0,
            failures: 0,
            last_recovery: now,
        }
    }

    /// Returns true if the peer is currently banned.
    pub fn is_banned(&self) -> bool {
        if let Some(ban_until) = self.ban_until {
            Instant::now() < ban_until
        } else {
            false
        }
    }

    /// Returns the remaining ban duration, if any.
    pub fn ban_remaining(&self) -> Option<Duration> {
        self.ban_until.and_then(|until| {
            let now = Instant::now();
            if now < until {
                Some(until - now)
            } else {
                None
            }
        })
    }

    /// Records a successful interaction.
    pub fn record_success(&mut self) {
        self.successes += 1;
        self.last_seen = Instant::now();
        self.score = self
            .score
            .saturating_add(Self::SUCCESS_BONUS)
            .min(Self::MAX_SCORE);
    }

    /// Records a violation and returns true if the peer should be banned.
    pub fn record_violation(&mut self, violation: ViolationType) -> bool {
        self.record_violation_with_context(violation, None)
    }

    /// Records a violation with context and returns true if the peer should be banned.
    pub fn record_violation_with_context(
        &mut self,
        violation: ViolationType,
        context: Option<String>,
    ) -> bool {
        self.failures += 1;
        self.last_seen = Instant::now();

        let record = if let Some(ctx) = context {
            ViolationRecord::with_context(violation, ctx)
        } else {
            ViolationRecord::new(violation)
        };
        self.violations.push(record);

        // Apply penalty
        let penalty = violation.penalty();
        self.score = self.score.saturating_sub(penalty);

        // Check if should be banned
        self.score < Self::BAN_THRESHOLD
    }

    /// Applies gradual score recovery over time.
    /// Call this periodically to allow good peers to recover reputation.
    pub fn apply_recovery(&mut self) {
        if self.is_banned() {
            return;
        }

        let now = Instant::now();
        let elapsed = now.duration_since(self.last_recovery);
        let intervals = elapsed.as_secs() / Self::RECOVERY_INTERVAL_SECS;

        if intervals > 0 {
            let recovery = (intervals as u8).saturating_mul(Self::RECOVERY_AMOUNT);
            self.score = self.score.saturating_add(recovery).min(Self::MAX_SCORE);
            self.last_recovery = now;
        }
    }

    /// Returns the success rate as a percentage (0-100).
    pub fn success_rate(&self) -> f64 {
        let total = self.successes + self.failures;
        if total == 0 {
            100.0
        } else {
            (self.successes as f64 / total as f64) * 100.0
        }
    }

    /// Returns the number of recent violations (within the last hour).
    pub fn recent_violation_count(&self) -> usize {
        let one_hour_ago = Instant::now() - Duration::from_secs(3600);
        self.violations
            .iter()
            .filter(|v| v.timestamp > one_hour_ago)
            .count()
    }

    /// Clears violation history older than the specified duration.
    pub fn clear_old_violations(&mut self, max_age: Duration) {
        let cutoff = Instant::now() - max_age;
        self.violations.retain(|v| v.timestamp > cutoff);
    }

    /// Manually bans the peer for the specified duration.
    pub fn ban(&mut self, duration: Duration) {
        self.ban_until = Some(Instant::now() + duration);
        self.score = 0;
    }

    /// Unbans the peer and resets their score to a low starting point.
    pub fn unban(&mut self) {
        self.ban_until = None;
        // Don't fully restore reputation - they need to earn it back
        self.score = Self::BAN_THRESHOLD;
    }
}

/// Configuration for the reputation manager.
#[derive(Clone, Debug)]
pub struct ReputationConfig {
    /// Default ban duration.
    pub default_ban_duration: Duration,
    /// Minimum ban duration.
    pub min_ban_duration: Duration,
    /// Maximum ban duration.
    pub max_ban_duration: Duration,
    /// How often to run cleanup of old data.
    pub cleanup_interval: Duration,
    /// Maximum age of violation records to keep.
    pub max_violation_age: Duration,
    /// Whether to enable gradual score recovery.
    pub enable_recovery: bool,
}

impl Default for ReputationConfig {
    fn default() -> Self {
        ReputationConfig {
            default_ban_duration: Duration::from_secs(3600), // 1 hour
            min_ban_duration: Duration::from_secs(300),      // 5 minutes
            max_ban_duration: Duration::from_secs(86400),    // 24 hours
            cleanup_interval: Duration::from_secs(600),      // 10 minutes
            max_violation_age: Duration::from_secs(86400),   // 24 hours
            enable_recovery: true,
        }
    }
}

/// Manages reputation for all peers.
pub struct ReputationManager {
    /// Per-peer reputation data.
    reputations: RwLock<HashMap<PeerId, PeerReputation>>,
    /// Configuration.
    config: ReputationConfig,
    /// Last cleanup time.
    last_cleanup: RwLock<Instant>,
}

impl ReputationManager {
    /// Creates a new reputation manager with default configuration.
    pub fn new() -> Self {
        Self::with_config(ReputationConfig::default())
    }

    /// Creates a new reputation manager with custom configuration.
    pub fn with_config(config: ReputationConfig) -> Self {
        ReputationManager {
            reputations: RwLock::new(HashMap::new()),
            config,
            last_cleanup: RwLock::new(Instant::now()),
        }
    }

    /// Gets or creates reputation data for a peer.
    fn get_or_create(&self, peer_id: &PeerId) -> PeerReputation {
        let reputations = self.reputations.read();
        if let Some(rep) = reputations.get(peer_id) {
            rep.clone()
        } else {
            drop(reputations);
            let rep = PeerReputation::new(*peer_id);
            self.reputations.write().insert(*peer_id, rep.clone());
            rep
        }
    }

    /// Records a successful interaction with a peer.
    pub fn record_success(&self, peer_id: &PeerId) {
        let mut reputations = self.reputations.write();
        let rep = reputations
            .entry(*peer_id)
            .or_insert_with(|| PeerReputation::new(*peer_id));

        if self.config.enable_recovery {
            rep.apply_recovery();
        }
        rep.record_success();
    }

    /// Records a violation by a peer.
    /// Returns true if the peer was auto-banned as a result.
    pub fn record_violation(&self, peer_id: &PeerId, violation: ViolationType) -> bool {
        self.record_violation_with_context(peer_id, violation, None)
    }

    /// Records a violation by a peer with additional context.
    /// Returns true if the peer was auto-banned as a result.
    pub fn record_violation_with_context(
        &self,
        peer_id: &PeerId,
        violation: ViolationType,
        context: Option<String>,
    ) -> bool {
        let mut reputations = self.reputations.write();
        let rep = reputations
            .entry(*peer_id)
            .or_insert_with(|| PeerReputation::new(*peer_id));

        let should_ban = rep.record_violation_with_context(violation, context);

        if should_ban && rep.ban_until.is_none() {
            // Calculate ban duration based on violation history
            let ban_duration = self.calculate_ban_duration(rep);
            rep.ban(ban_duration);
            true
        } else {
            false
        }
    }

    /// Calculates ban duration based on peer's violation history.
    fn calculate_ban_duration(&self, rep: &PeerReputation) -> Duration {
        let recent_violations = rep.recent_violation_count();

        // Exponential backoff: longer bans for repeat offenders
        let multiplier = match recent_violations {
            0..=2 => 1,
            3..=5 => 2,
            6..=10 => 4,
            _ => 8,
        };

        let duration = self.config.default_ban_duration * multiplier;
        duration.clamp(self.config.min_ban_duration, self.config.max_ban_duration)
    }

    /// Checks if a peer is currently banned.
    pub fn is_banned(&self, peer_id: &PeerId) -> bool {
        let reputations = self.reputations.read();
        if let Some(rep) = reputations.get(peer_id) {
            rep.is_banned()
        } else {
            false
        }
    }

    /// Gets the reputation score for a peer.
    /// Returns None if the peer is unknown.
    pub fn get_score(&self, peer_id: &PeerId) -> Option<u8> {
        let mut reputations = self.reputations.write();
        if let Some(rep) = reputations.get_mut(peer_id) {
            if self.config.enable_recovery {
                rep.apply_recovery();
            }
            Some(rep.score)
        } else {
            None
        }
    }

    /// Gets the full reputation data for a peer.
    pub fn get_reputation(&self, peer_id: &PeerId) -> Option<PeerReputation> {
        let mut reputations = self.reputations.write();
        if let Some(rep) = reputations.get_mut(peer_id) {
            if self.config.enable_recovery {
                rep.apply_recovery();
            }
            Some(rep.clone())
        } else {
            None
        }
    }

    /// Manually bans a peer for the default duration.
    pub fn ban_peer(&self, peer_id: &PeerId) {
        self.ban_peer_for(peer_id, self.config.default_ban_duration);
    }

    /// Manually bans a peer for a specific duration.
    pub fn ban_peer_for(&self, peer_id: &PeerId, duration: Duration) {
        let mut reputations = self.reputations.write();
        let rep = reputations
            .entry(*peer_id)
            .or_insert_with(|| PeerReputation::new(*peer_id));
        rep.ban(duration);
    }

    /// Unbans a peer.
    pub fn unban_peer(&self, peer_id: &PeerId) {
        let mut reputations = self.reputations.write();
        if let Some(rep) = reputations.get_mut(peer_id) {
            rep.unban();
        }
    }

    /// Gets all currently banned peers.
    pub fn banned_peers(&self) -> Vec<PeerId> {
        self.reputations
            .read()
            .iter()
            .filter(|(_, rep)| rep.is_banned())
            .map(|(id, _)| *id)
            .collect()
    }

    /// Gets peers with low reputation (at risk of being banned).
    pub fn low_reputation_peers(&self, threshold: u8) -> Vec<(PeerId, u8)> {
        self.reputations
            .read()
            .iter()
            .filter(|(_, rep)| rep.score < threshold && !rep.is_banned())
            .map(|(id, rep)| (*id, rep.score))
            .collect()
    }

    /// Gets peers sorted by reputation score (highest first).
    pub fn peers_by_reputation(&self) -> Vec<(PeerId, u8)> {
        let mut peers: Vec<_> = self
            .reputations
            .read()
            .iter()
            .filter(|(_, rep)| !rep.is_banned())
            .map(|(id, rep)| (*id, rep.score))
            .collect();
        peers.sort_by(|a, b| b.1.cmp(&a.1));
        peers
    }

    /// Removes reputation data for a peer.
    pub fn remove_peer(&self, peer_id: &PeerId) {
        self.reputations.write().remove(peer_id);
    }

    /// Runs periodic cleanup of old data.
    pub fn cleanup(&self) {
        let now = Instant::now();

        // Check if cleanup is needed
        {
            let last_cleanup = self.last_cleanup.read();
            if now.duration_since(*last_cleanup) < self.config.cleanup_interval {
                return;
            }
        }

        // Update last cleanup time
        *self.last_cleanup.write() = now;

        // Clean up old violations and expired bans
        let mut reputations = self.reputations.write();
        for rep in reputations.values_mut() {
            rep.clear_old_violations(self.config.max_violation_age);

            // Clear expired bans
            if let Some(ban_until) = rep.ban_until {
                if now >= ban_until {
                    rep.ban_until = None;
                    // Restore to ban threshold score after ban expires
                    rep.score = PeerReputation::BAN_THRESHOLD;
                }
            }

            // Apply recovery
            if self.config.enable_recovery {
                rep.apply_recovery();
            }
        }
    }

    /// Returns statistics about peer reputations.
    pub fn stats(&self) -> ReputationStats {
        let reputations = self.reputations.read();
        let total = reputations.len();
        let banned = reputations.values().filter(|r| r.is_banned()).count();
        let low_rep = reputations
            .values()
            .filter(|r| r.score < 30 && !r.is_banned())
            .count();
        let high_rep = reputations.values().filter(|r| r.score >= 80).count();

        let avg_score = if total > 0 {
            reputations.values().map(|r| r.score as u64).sum::<u64>() / total as u64
        } else {
            50
        } as u8;

        ReputationStats {
            total_peers: total,
            banned_peers: banned,
            low_reputation_peers: low_rep,
            high_reputation_peers: high_rep,
            average_score: avg_score,
        }
    }
}

impl Default for ReputationManager {
    fn default() -> Self {
        Self::new()
    }
}

/// Statistics about peer reputations.
#[derive(Clone, Debug)]
pub struct ReputationStats {
    /// Total number of tracked peers.
    pub total_peers: usize,
    /// Number of currently banned peers.
    pub banned_peers: usize,
    /// Number of peers with low reputation.
    pub low_reputation_peers: usize,
    /// Number of peers with high reputation.
    pub high_reputation_peers: usize,
    /// Average reputation score.
    pub average_score: u8,
}

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

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

    #[test]
    fn test_peer_reputation_new() {
        let peer_id = random_peer_id();
        let rep = PeerReputation::new(peer_id);

        assert_eq!(rep.score, PeerReputation::INITIAL_SCORE);
        assert!(!rep.is_banned());
        assert_eq!(rep.successes, 0);
        assert_eq!(rep.failures, 0);
    }

    #[test]
    fn test_record_success() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);

        rep.record_success();
        assert_eq!(rep.successes, 1);
        assert_eq!(
            rep.score,
            PeerReputation::INITIAL_SCORE + PeerReputation::SUCCESS_BONUS
        );
    }

    #[test]
    fn test_record_violation() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);

        let penalty = ViolationType::InvalidMessage.penalty();
        let should_ban = rep.record_violation(ViolationType::InvalidMessage);

        assert!(!should_ban); // Score is still above threshold
        assert_eq!(rep.failures, 1);
        assert_eq!(rep.violations.len(), 1);
        assert_eq!(rep.score, PeerReputation::INITIAL_SCORE - penalty);
    }

    #[test]
    fn test_auto_ban_on_low_score() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);

        // Apply multiple severe violations to trigger ban
        let should_ban1 = rep.record_violation(ViolationType::ProtocolViolation); // -25
        let should_ban2 = rep.record_violation(ViolationType::InvalidData); // -30

        // First violation shouldn't ban (50 - 25 = 25)
        assert!(!should_ban1);
        // Second violation should trigger ban (25 - 30 = 0)
        assert!(should_ban2);
        assert!(rep.score < PeerReputation::BAN_THRESHOLD);
    }

    #[test]
    fn test_score_max_limit() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);
        rep.score = 99;

        rep.record_success();
        rep.record_success();

        assert_eq!(rep.score, PeerReputation::MAX_SCORE);
    }

    #[test]
    fn test_score_min_limit() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);
        rep.score = 5;

        rep.record_violation(ViolationType::InvalidData); // -30

        assert_eq!(rep.score, 0);
    }

    #[test]
    fn test_manual_ban() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);

        rep.ban(Duration::from_secs(3600));

        assert!(rep.is_banned());
        assert!(rep.ban_remaining().is_some());
    }

    #[test]
    fn test_unban() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);

        rep.ban(Duration::from_secs(3600));
        assert!(rep.is_banned());

        rep.unban();
        assert!(!rep.is_banned());
        assert_eq!(rep.score, PeerReputation::BAN_THRESHOLD);
    }

    #[test]
    fn test_reputation_manager_record_success() {
        let manager = ReputationManager::new();
        let peer_id = random_peer_id();

        manager.record_success(&peer_id);

        let score = manager.get_score(&peer_id).unwrap();
        assert_eq!(
            score,
            PeerReputation::INITIAL_SCORE + PeerReputation::SUCCESS_BONUS
        );
    }

    #[test]
    fn test_reputation_manager_record_violation() {
        let manager = ReputationManager::new();
        let peer_id = random_peer_id();

        let banned = manager.record_violation(&peer_id, ViolationType::SlowResponse);

        assert!(!banned);
        let score = manager.get_score(&peer_id).unwrap();
        assert_eq!(
            score,
            PeerReputation::INITIAL_SCORE - ViolationType::SlowResponse.penalty()
        );
    }

    #[test]
    fn test_reputation_manager_auto_ban() {
        let manager = ReputationManager::new();
        let peer_id = random_peer_id();

        // Apply severe violations to trigger auto-ban
        manager.record_violation(&peer_id, ViolationType::InvalidData); // -30
        let banned = manager.record_violation(&peer_id, ViolationType::InvalidData); // -30

        assert!(banned);
        assert!(manager.is_banned(&peer_id));
    }

    #[test]
    fn test_reputation_manager_manual_ban() {
        let manager = ReputationManager::new();
        let peer_id = random_peer_id();

        manager.ban_peer(&peer_id);

        assert!(manager.is_banned(&peer_id));
    }

    #[test]
    fn test_reputation_manager_unban() {
        let manager = ReputationManager::new();
        let peer_id = random_peer_id();

        manager.ban_peer(&peer_id);
        assert!(manager.is_banned(&peer_id));

        manager.unban_peer(&peer_id);
        assert!(!manager.is_banned(&peer_id));
    }

    #[test]
    fn test_banned_peers_list() {
        let manager = ReputationManager::new();
        let peer1 = random_peer_id();
        let peer2 = random_peer_id();
        let peer3 = random_peer_id();

        manager.ban_peer(&peer1);
        manager.ban_peer(&peer2);
        manager.record_success(&peer3);

        let banned = manager.banned_peers();
        assert_eq!(banned.len(), 2);
        assert!(banned.contains(&peer1));
        assert!(banned.contains(&peer2));
        assert!(!banned.contains(&peer3));
    }

    #[test]
    fn test_peers_by_reputation() {
        let manager = ReputationManager::new();
        let peer1 = random_peer_id();
        let peer2 = random_peer_id();

        // Give peer1 higher reputation
        for _ in 0..10 {
            manager.record_success(&peer1);
        }
        manager.record_violation(&peer2, ViolationType::SlowResponse);

        let peers = manager.peers_by_reputation();
        assert!(!peers.is_empty());

        // peer1 should be first (higher score)
        if peers.len() >= 2 {
            assert!(peers[0].1 > peers[1].1);
        }
    }

    #[test]
    fn test_reputation_stats() {
        let manager = ReputationManager::new();
        let peer1 = random_peer_id();
        let peer2 = random_peer_id();

        manager.record_success(&peer1);
        manager.ban_peer(&peer2);

        let stats = manager.stats();
        assert_eq!(stats.total_peers, 2);
        assert_eq!(stats.banned_peers, 1);
    }

    #[test]
    fn test_violation_types() {
        // Test all violation types have penalties
        let violations = [
            ViolationType::InvalidMessage,
            ViolationType::SlowResponse,
            ViolationType::Spam,
            ViolationType::ProtocolViolation,
            ViolationType::DuplicateData,
            ViolationType::InvalidData,
            ViolationType::UnexpectedDisconnect,
            ViolationType::NoResponse,
        ];

        for v in violations {
            assert!(v.penalty() > 0);
            assert!(!v.description().is_empty());
        }
    }

    #[test]
    fn test_success_rate() {
        let peer_id = random_peer_id();
        let mut rep = PeerReputation::new(peer_id);

        // 100% when no interactions
        assert_eq!(rep.success_rate(), 100.0);

        // 75% with 3 successes and 1 failure
        rep.record_success();
        rep.record_success();
        rep.record_success();
        rep.record_violation(ViolationType::SlowResponse);

        assert!((rep.success_rate() - 75.0).abs() < 0.01);
    }
}
