"""
Network class to retrieve network interface information and manipulate IP addresses.
No third-party libraries required, uses /sys filesystem and ioctl system calls.
"""
import os
import re
import struct
import socket
import fcntl
from typing import Optional, Tuple, List, Union


class Network:
    def __init__(self):
        self.iface: Optional[str] = None
        self._ip: Optional[str] = None
        self._netmask: Optional[str] = None
        self._network: Optional[str] = None
        self._gateway: Optional[str] = None
        self._mac: Optional[str] = None
        self._bytes_rx: Optional[int] = None
        self._bytes_tx: Optional[int] = None
        self._cidr: Optional[int] = None
        self._ipv6: Optional[List[str]] = None
        self._manual_ip: bool = False  # Flag to track if IP was manually set
        self._manual_netmask: bool = False  # Flag to track if netmask was manually set

    def setiface(self, iface: str) -> 'Network':
        """Set the network interface and refresh all information."""
        self.iface = iface
        # Reset manual flags when changing interface
        self._manual_ip = False
        self._manual_netmask = False
        self._refresh_all()
        return self
    
    def setip(self, ip: str) -> 'Network':
        """Manually set the IPv4 address (overrides system value)."""
        self._ip = ip
        self._manual_ip = True
        # Update dependent values
        if self._manual_netmask:
            self._update_dependent_values()
        else:
            # Only update network and cidr if we have netmask
            self._network = self.getnetwork()
            self._cidr = self.getcidr()
        return self
    
    def setnetmask(self, netmask: str) -> 'Network':
        """Manually set the netmask (overrides system value)."""
        self._netmask = netmask
        self._manual_netmask = True
        # Update dependent values
        if self._manual_ip:
            self._update_dependent_values()
        else:
            # Only update network and cidr if we have IP
            self._network = self.getnetwork()
            self._cidr = self.getcidr()
        return self

    def _update_dependent_values(self):
        """Update network and CIDR based on current IP and netmask."""
        if self._ip and self._netmask:
            self._network = self._calculate_network(self._ip, self._netmask)
            self._cidr = self._calculate_cidr(self._netmask)
        else:
            self._network = None
            self._cidr = None

    def _refresh_all(self):
        """Refresh all interface information respecting manual overrides."""
        if not self.iface:
            return
            
        # Only get from system if not manually set
        if not self._manual_ip:
            self._ip = self.getip()
        if not self._manual_netmask:
            self._netmask = self.getnetmask()
        
        # Always refresh these from system (they can't be manually set)
        self._gateway = self.getgateway()
        self._mac = self.getmac()
        self._bytes_rx, self._bytes_tx = self.get_transfer_bytes()
        self._ipv6 = self.getipv6()
        
        # Update dependent values
        self._update_dependent_values()

    # ------------------------------------------------------------------
    # Public getter methods
    # ------------------------------------------------------------------
    def getip(self) -> Optional[str]:
        """Get IPv4 address of the interface."""
        if self._manual_ip:
            return self._ip
        return self._get_ip_from_ioctl()

    def getnetmask(self) -> Optional[str]:
        """Get netmask of the interface."""
        if self._manual_netmask:
            return self._netmask
        return self._get_netmask_from_ioctl()

    def getnetwork(self) -> Optional[str]:
        """Calculate network address from IP and netmask."""
        if self._network is not None:
            return self._network
        ip = self.getip()
        mask = self.getnetmask()
        return self._calculate_network(ip, mask)

    def _calculate_network(self, ip: Optional[str], mask: Optional[str]) -> Optional[str]:
        """Calculate network address from IP and netmask."""
        if not ip or not mask:
            return None
        try:
            ip_int = self._ip_to_int(ip)
            mask_int = self._ip_to_int(mask)
            network_int = ip_int & mask_int
            return self._int_to_ip(network_int)
        except Exception:
            return None

    def getgateway(self) -> Optional[str]:
        """Get default gateway for the interface."""
        return self._get_gateway_from_proc()

    def getmac(self) -> Optional[str]:
        """Get MAC address of the interface."""
        return self._get_mac_from_sys()

    def get_transfer_bytes(self) -> Tuple[Optional[int], Optional[int]]:
        """Get total received and transmitted bytes (raw integers)."""
        rx, tx = self._get_stats_from_sys()
        return rx, tx

    def get_transfer_human(self) -> Tuple[Optional[str], Optional[str]]:
        """Get total received and transmitted bytes in human-readable format."""
        rx, tx = self.get_transfer_bytes()
        return self._bytes_to_human(rx), self._bytes_to_human(tx)

    def getcidr(self) -> Optional[int]:
        """Get CIDR prefix length from netmask."""
        if self._cidr is not None:
            return self._cidr
        mask = self.getnetmask()
        return self._calculate_cidr(mask)

    def _calculate_cidr(self, mask: Optional[str]) -> Optional[int]:
        """Calculate CIDR from netmask."""
        if not mask:
            return None
        try:
            mask_int = self._ip_to_int(mask)
            # Count leading 1 bits
            cidr = bin(mask_int).count('1')
            return cidr if 0 <= cidr <= 32 else None
        except Exception:
            return None

    def getipv6(self) -> Optional[List[str]]:
        """Get all IPv6 addresses of the interface (link-local and global)."""
        return self._get_ipv6_from_proc()

    # ------------------------------------------------------------------
    # IP iteration (inc parameter)
    # ------------------------------------------------------------------
    def iter_ip(self, inc: int = 1, ip: Optional[str] = None, netmask: Optional[str] = None) -> Optional[str]:
        """
        Return next or previous IP address based on increment.
        
        Args:
            inc: Increment step (positive for forward, negative for backward)
            ip: Optional custom IP address (uses current IP if not provided)
            netmask: Optional custom netmask (uses current netmask if not provided)
        
        Examples:
            net.iter_ip(1)                    # Use current IP and netmask
            net.iter_ip(-1, '10.1.1.1', '255.255.255.0')  # Custom IP and netmask
            net.iter_ip(2, ip='192.168.1.1', netmask='24')  # CIDR notation supported
        """
        # Use provided values or fall back to current settings
        use_ip = ip if ip is not None else self.getip()
        use_netmask = netmask if netmask is not None else self.getnetmask()
        
        # Convert CIDR notation to netmask if needed
        if use_netmask and isinstance(use_netmask, str) and '/' not in use_netmask:
            if use_netmask.isdigit() and 0 <= int(use_netmask) <= 32:
                use_netmask = self._cidr_to_netmask(int(use_netmask))
        
        if not use_ip or not use_netmask:
            return None
        
        try:
            ip_int = self._ip_to_int(use_ip)
            mask_int = self._ip_to_int(use_netmask)
            network_int = ip_int & mask_int
            broadcast_int = network_int | (~mask_int & 0xFFFFFFFF)
            
            # Calculate new IP
            new_ip_int = ip_int + inc
            
            # Clamp to network range (exclude network and broadcast addresses)
            if new_ip_int <= network_int:
                # Wrap to the end of network (minus broadcast)
                new_ip_int = broadcast_int - 1
            elif new_ip_int >= broadcast_int:
                # Wrap to the start of network (plus network address)
                new_ip_int = network_int + 1
            
            # Ensure we don't return network or broadcast address
            if new_ip_int == network_int:
                new_ip_int = broadcast_int - 1
            elif new_ip_int == broadcast_int:
                new_ip_int = network_int + 1
            
            return self._int_to_ip(new_ip_int)
        except Exception:
            return None
    
    def iter_ip_range(self, start_ip: str, netmask: str, steps: int = 10) -> List[str]:
        """
        Generate a range of IP addresses.
        
        Args:
            start_ip: Starting IP address
            netmask: Netmask (dotted decimal or CIDR)
            steps: Number of steps to generate (can be positive or negative)
        
        Returns:
            List of IP addresses
        """
        results = []
        for i in range(1, abs(steps) + 1):
            step = i if steps > 0 else -i
            next_ip = self.iter_ip(step, start_ip, netmask)
            if next_ip:
                results.append(next_ip)
        return results

    @staticmethod
    def _cidr_to_netmask(cidr: int) -> str:
        """Convert CIDR prefix length to dotted netmask."""
        mask_int = (0xFFFFFFFF << (32 - cidr)) & 0xFFFFFFFF
        return f"{(mask_int >> 24) & 0xFF}.{(mask_int >> 16) & 0xFF}.{(mask_int >> 8) & 0xFF}.{mask_int & 0xFF}"

    # ------------------------------------------------------------------
    # Low-level implementation methods (with fallbacks)
    # ------------------------------------------------------------------
    def _get_ip_from_ioctl(self) -> Optional[str]:
        """Get IPv4 address using ioctl SIOCGIFADDR."""
        if not self.iface:
            return None
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            ifreq = struct.pack('16s256s', self.iface.encode(), b'\x00'*256)
            res = fcntl.ioctl(sock.fileno(), 0x8915, ifreq)  # SIOCGIFADDR
            sock.close()
            ip_bytes = res[20:24]
            return socket.inet_ntoa(ip_bytes)
        except Exception:
            return self._get_ip_from_sys()

    def _get_ip_from_sys(self) -> Optional[str]:
        """Fallback: read IP from /sys/class/net/<iface>/address."""
        if not self.iface:
            return None
        path = f'/sys/class/net/{self.iface}/address'
        # Note: /sys doesn't directly provide IPv4, try /proc instead
        return self._get_ip_from_proc()

    def _get_ip_from_proc(self) -> Optional[str]:
        """Read IPv4 from /proc/net/fib_trie or /proc/net/route."""
        if not self.iface:
            return None
        try:
            with open('/proc/net/fib_trie', 'r') as f:
                content = f.read()
            # Parse the trie for the interface
            lines = content.split('\n')
            in_correct_host = False
            for line in lines:
                if f"|-- {self.iface}" in line:
                    in_correct_host = True
                elif in_correct_host and '|--' in line and 'host' in line:
                    parts = line.split()
                    for part in parts:
                        if re.match(r'^\d+\.\d+\.\d+\.\d+$', part):
                            return part
                    in_correct_host = False
        except Exception:
            pass
        return None

    def _get_netmask_from_ioctl(self) -> Optional[str]:
        """Get netmask using ioctl SIOCGIFNETMASK."""
        if not self.iface:
            return None
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            ifreq = struct.pack('16s256s', self.iface.encode(), b'\x00'*256)
            res = fcntl.ioctl(sock.fileno(), 0x891b, ifreq)  # SIOCGIFNETMASK
            sock.close()
            mask_bytes = res[20:24]
            return socket.inet_ntoa(mask_bytes)
        except Exception:
            return None

    def _get_mac_from_sys(self) -> Optional[str]:
        """Read MAC address from /sys/class/net/<iface>/address."""
        if not self.iface:
            return None
        path = f'/sys/class/net/{self.iface}/address'
        try:
            with open(path, 'r') as f:
                mac = f.read().strip()
                if re.match(r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}', mac):
                    return mac.upper()
        except Exception:
            pass
        return None

    def _get_stats_from_sys(self) -> Tuple[Optional[int], Optional[int]]:
        """Read rx_bytes and tx_bytes from /sys/class/net/<iface>/statistics/."""
        if not self.iface:
            return None, None
        rx_path = f'/sys/class/net/{self.iface}/statistics/rx_bytes'
        tx_path = f'/sys/class/net/{self.iface}/statistics/tx_bytes'
        rx = self._read_int_file(rx_path)
        tx = self._read_int_file(tx_path)
        return rx, tx

    def _get_gateway_from_proc(self) -> Optional[str]:
        """Parse /proc/net/route to find default gateway for the interface."""
        if not self.iface:
            return None
        try:
            with open('/proc/net/route', 'r') as f:
                lines = f.readlines()[1:]  # skip header
            for line in lines:
                parts = line.strip().split()
                if len(parts) >= 3 and parts[0] == self.iface:
                    dest = int(parts[1], 16)
                    gateway = int(parts[2], 16)
                    if dest == 0:  # default route
                        return self._int_to_ip(gateway)
        except Exception:
            pass
        return None

    def _get_ipv6_from_proc(self) -> Optional[List[str]]:
        """Parse /proc/net/if_inet6 for IPv6 addresses of the interface."""
        if not self.iface:
            return None
        ipv6_list = []
        try:
            with open('/proc/net/if_inet6', 'r') as f:
                for line in f:
                    parts = line.strip().split()
                    if len(parts) >= 4 and parts[3] == self.iface:
                        ipv6_hex = parts[0]
                        # Convert hex to IPv6 notation
                        ipv6 = ':'.join(ipv6_hex[i:i+4] for i in range(0, 32, 4))
                        # Compress IPv6 (optional)
                        ipv6 = self._compress_ipv6(ipv6)
                        ipv6_list.append(ipv6)
            return ipv6_list if ipv6_list else None
        except Exception:
            return None

    @staticmethod
    def _compress_ipv6(ipv6: str) -> str:
        """Compress IPv6 address."""
        try:
            # Expand to full 8 groups
            groups = ipv6.split(':')
            if len(groups) < 8:
                groups = ['0'] * (8 - len(groups)) + groups
            # Find longest run of zeros
            best_start = -1
            best_len = 0
            current_start = -1
            current_len = 0
            for i, group in enumerate(groups):
                if group == '0':
                    if current_start == -1:
                        current_start = i
                    current_len += 1
                else:
                    if current_len > best_len:
                        best_start = current_start
                        best_len = current_len
                    current_start = -1
                    current_len = 0
            if current_len > best_len:
                best_start = current_start
                best_len = current_len
            
            if best_len > 1:
                # Compress
                start = best_start
                end = best_start + best_len
                compressed = groups[:start] + [''] + groups[end:]
                ipv6 = ':'.join(compressed).replace(':::', '::')
            else:
                # Remove leading zeros from each group
                ipv6 = ':'.join(g.lstrip('0') or '0' for g in groups)
            return ipv6
        except Exception:
            return ipv6

    @staticmethod
    def _read_int_file(path: str) -> Optional[int]:
        """Read integer from a file."""
        try:
            with open(path, 'r') as f:
                return int(f.read().strip())
        except Exception:
            return None

    @staticmethod
    def _ip_to_int(ip: str) -> int:
        """Convert dotted IPv4 to integer."""
        parts = ip.split('.')
        return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])

    @staticmethod
    def _int_to_ip(ip_int: int) -> str:
        """Convert integer to dotted IPv4."""
        return f"{(ip_int >> 24) & 0xFF}.{(ip_int >> 16) & 0xFF}.{(ip_int >> 8) & 0xFF}.{ip_int & 0xFF}"

    @staticmethod
    def _bytes_to_human(bytes_val: Optional[int]) -> Optional[str]:
        """Convert bytes to human-readable format."""
        if bytes_val is None:
            return None
        for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB']:
            if bytes_val < 1024.0:
                return f"{bytes_val:.2f} {unit}"
            bytes_val /= 1024.0
        return f"{bytes_val:.2f} PiB"

    # ------------------------------------------------------------------
    # Utility methods
    # ------------------------------------------------------------------
    def reset_to_system(self) -> 'Network':
        """Reset manually set values back to system values."""
        self._manual_ip = False
        self._manual_netmask = False
        self._refresh_all()
        return self

    def __repr__(self) -> str:
        if not self.iface:
            return "Network(no interface set)"
        manual_info = []
        if self._manual_ip:
            manual_info.append("manual_ip")
        if self._manual_netmask:
            manual_info.append("manual_netmask")
        manual_str = f", manual={manual_info}" if manual_info else ""
        return (f"Network(iface={self.iface}, ip={self._ip}, netmask={self._netmask}, "
                f"gateway={self._gateway}, mac={self._mac}, cidr={self._cidr}, "
                f"ipv6={self._ipv6}{manual_str})")


# ------------------------------------------------------------------
# Example usage and test
# ------------------------------------------------------------------
if __name__ == '__main__':
    net = Network()
    net.setiface('eth0')  # Change to your interface (e.g., 'wlan0', 'enp0s3')
    
    print("=== System Interface Information ===")
    print(f"Interface: {net.iface}")
    print(f"IP: {net.getip()}")
    print(f"Netmask: {net.getnetmask()}")
    print(f"Network: {net.getnetwork()}")
    print(f"Gateway: {net.getgateway()}")
    print(f"MAC: {net.getmac()}")
    print(f"CIDR: {net.getcidr()}")
    print(f"IPv6: {net.getipv6()}")
    
    rx, tx = net.get_transfer_human()
    print(f"RX bytes: {rx}")
    print(f"TX bytes: {tx}")
    
    print("\n=== Testing Custom IP/Netmask ===")
    # Set custom IP and netmask
    net.setip('10.1.1.1')
    net.setnetmask('255.255.255.0')
    # Or use CIDR notation: net.setnetmask('24')
    
    print(f"Current IP: {net.getip()} (manually set)")
    print(f"Current Netmask: {net.getnetmask()} (manually set)")
    print(f"Network: {net.getnetwork()}")
    print(f"CIDR: {net.getcidr()}")
    
    print("\n=== IP Iteration Tests ===")
    # Test with current settings
    print(f"Using IP {net.getip()}, Netmask {net.getnetmask()}:")
    print(f"inc=1  -> {net.iter_ip(1)}")
    print(f"inc=2  -> {net.iter_ip(2)}")
    print(f"inc=-1 -> {net.iter_ip(-1)}")
    print(f"inc=-2 -> {net.iter_ip(-2)}")
    
    # Test with custom values directly in iter_ip
    print("\n=== Custom Values in iter_ip ===")
    print(f"iter_ip(1, '192.168.1.1', '24'): {net.iter_ip(1, '192.168.1.1', '24')}")
    print(f"iter_ip(-1, '192.168.1.1', '255.255.255.0'): {net.iter_ip(-1, '192.168.1.1', '255.255.255.0')}")
    print(f"iter_ip(5, '10.0.0.1', '255.255.0.0'): {net.iter_ip(5, '10.0.0.1', '255.255.0.0')}")
    
    # Generate a range of IPs
    print("\n=== IP Range Generation ===")
    range_ips = net.iter_ip_range('10.1.1.1', '24', 5)
    print(f"Next 5 IPs: {range_ips}")
    
    # Reset back to system values
    net.reset_to_system()
    print(f"\nAfter reset: IP={net.getip()} (back to system value)")