//! Dynamic Network Configuration & IPAM
//!
//! Each Master Node manages its own /16 subnet: 10.X.0.0/16
//! where X is the unique Master Node identifier (0-255)

use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::net::Ipv4Addr;
use std::sync::{Arc, Mutex};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum IpamError {
    #[error("invalid network octet: must be 0-255, got {0}")]
    InvalidOctet(u8),
    #[error("reserved network range: {0}")]
    ReservedRange(String),
    #[error("no available IPs in pool")]
    PoolExhausted,
    #[error("IP already allocated: {0}")]
    AlreadyAllocated(Ipv4Addr),
}

/// Network configuration for a Master Node
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
    /// Second octet of 10.X.0.0/16 network
    pub master_octet: u8,
    /// Full network prefix: 10.X.0.0/16
    pub network: String,
    /// Master node IP (always 10.X.0.1)
    pub master_ip: Ipv4Addr,
    /// Gateway IP (always 10.X.0.1)
    pub gateway: Ipv4Addr,
    /// Network mask
    pub netmask: Ipv4Addr,
    /// CIDR notation
    pub cidr: u8,
}

impl NetworkConfig {
    /// Create a new network configuration for a Master Node
    pub fn new(master_octet: u8) -> Result<Self, IpamError> {
        // Check for reserved ranges
        if is_reserved_octet(master_octet) {
            return Err(IpamError::ReservedRange(format!("10.{}.0.0/16", master_octet)));
        }

        let master_ip = Ipv4Addr::new(10, master_octet, 0, 1);
        let gateway = master_ip;
        let netmask = Ipv4Addr::new(255, 255, 0, 0);

        Ok(Self {
            master_octet,
            network: format!("10.{}.0.0/16", master_octet),
            master_ip,
            gateway,
            netmask,
            cidr: 16,
        })
    }

    /// Get the base network address
    pub fn network_addr(&self) -> Ipv4Addr {
        Ipv4Addr::new(10, self.master_octet, 0, 0)
    }

    /// Get broadcast address
    pub fn broadcast_addr(&self) -> Ipv4Addr {
        Ipv4Addr::new(10, self.master_octet, 255, 255)
    }

    /// Check if IP belongs to this network
    pub fn contains(&self, ip: &Ipv4Addr) -> bool {
        let octets = ip.octets();
        octets[0] == 10 && octets[1] == self.master_octet
    }
}

/// IP Address Management for dynamic allocation
pub struct IpamPool {
    config: NetworkConfig,
    allocated: Arc<Mutex<HashSet<Ipv4Addr>>>,
    next_third_octet: Arc<Mutex<u8>>,
    next_fourth_octet: Arc<Mutex<u8>>,
}

impl IpamPool {
    pub fn new(config: NetworkConfig) -> Self {
        let mut allocated = HashSet::new();
        // Reserve master node IP
        allocated.insert(config.master_ip);
        // Reserve network and broadcast addresses
        allocated.insert(config.network_addr());
        allocated.insert(config.broadcast_addr());

        Self {
            config,
            allocated: Arc::new(Mutex::new(allocated)),
            next_third_octet: Arc::new(Mutex::new(0)),
            next_fourth_octet: Arc::new(Mutex::new(2)), // Start from .0.2 (after master at .0.1)
        }
    }

    /// Allocate the next available IP address
    pub fn allocate(&self) -> Result<Ipv4Addr, IpamError> {
        let mut allocated = self.allocated.lock().unwrap();
        let mut third = self.next_third_octet.lock().unwrap();
        let mut fourth = self.next_fourth_octet.lock().unwrap();

        // Try to find next available IP
        for _ in 0..65534 {  // Max addresses in /16 minus reserved
            let ip = Ipv4Addr::new(10, self.config.master_octet, *third, *fourth);

            if !allocated.contains(&ip) && !is_reserved_ip(&ip) {
                allocated.insert(ip);
                
                // Increment for next allocation
                *fourth += 1;
                if *fourth == 0 {  // Wrapped around
                    *third += 1;
                    *fourth = 1;
                }

                tracing::info!("Allocated IP: {} (pool size: {})", ip, allocated.len());
                return Ok(ip);
            }

            // Move to next IP
            *fourth += 1;
            if *fourth == 0 {
                *third += 1;
                *fourth = 1;
            }
        }

        Err(IpamError::PoolExhausted)
    }

    /// Release an allocated IP back to the pool
    pub fn release(&self, ip: Ipv4Addr) -> Result<(), IpamError> {
        let mut allocated = self.allocated.lock().unwrap();

        if !allocated.remove(&ip) {
            return Err(IpamError::AlreadyAllocated(ip));
        }

        tracing::info!("Released IP: {} (pool size: {})", ip, allocated.len());
        Ok(())
    }

    /// Get count of allocated IPs
    pub fn allocated_count(&self) -> usize {
        self.allocated.lock().unwrap().len()
    }

    /// Check if IP is allocated
    pub fn is_allocated(&self, ip: &Ipv4Addr) -> bool {
        self.allocated.lock().unwrap().contains(ip)
    }
}

/// Check if octet is in a reserved range
fn is_reserved_octet(octet: u8) -> bool {
    matches!(octet,
        0 |      // 10.0.0.0/8 - often used for private networks
        255      // Avoid broadcast-like ranges
    )
}

/// Check if IP should be reserved (broadcast, network, multicast)
fn is_reserved_ip(ip: &Ipv4Addr) -> bool {
    let octets = ip.octets();
    
    // Network address (x.x.0.0)
    if octets[2] == 0 && octets[3] == 0 {
        return true;
    }
    
    // Broadcast address (x.x.255.255)
    if octets[2] == 255 && octets[3] == 255 {
        return true;
    }
    
    // Subnet broadcasts (x.x.x.255)
    if octets[3] == 255 {
        return true;
    }
    
    false
}

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

    #[test]
    fn test_network_config_creation() {
        let config = NetworkConfig::new(42).unwrap();
        assert_eq!(config.master_octet, 42);
        assert_eq!(config.network, "10.42.0.0/16");
        assert_eq!(config.master_ip, Ipv4Addr::new(10, 42, 0, 1));
    }

    #[test]
    fn test_reserved_octets() {
        assert!(NetworkConfig::new(0).is_err());
        assert!(NetworkConfig::new(255).is_err());
        assert!(NetworkConfig::new(1).is_ok());
    }

    #[test]
    fn test_ip_allocation() {
        let config = NetworkConfig::new(42).unwrap();
        let pool = IpamPool::new(config);

        let ip1 = pool.allocate().unwrap();
        let ip2 = pool.allocate().unwrap();

        assert_ne!(ip1, ip2);
        assert_eq!(ip1, Ipv4Addr::new(10, 42, 0, 2));
        assert_eq!(ip2, Ipv4Addr::new(10, 42, 0, 3));
    }

    #[test]
    fn test_ip_release() {
        let config = NetworkConfig::new(42).unwrap();
        let pool = IpamPool::new(config);

        let ip = pool.allocate().unwrap();
        assert!(pool.is_allocated(&ip));

        pool.release(ip).unwrap();
        assert!(!pool.is_allocated(&ip));
    }

    #[test]
    fn test_network_contains() {
        let config = NetworkConfig::new(42).unwrap();
        
        assert!(config.contains(&Ipv4Addr::new(10, 42, 0, 1)));
        assert!(config.contains(&Ipv4Addr::new(10, 42, 100, 200)));
        assert!(!config.contains(&Ipv4Addr::new(10, 43, 0, 1)));
        assert!(!config.contains(&Ipv4Addr::new(192, 168, 1, 1)));
    }
}
