import dataclasses
from typing import List, Optional

from forcen_public_utils.math_types import UInt8


class IPv4:
    def __init__(self, num_1: UInt8, num_2: UInt8, num_3: UInt8, num_4: UInt8):
        self._address_list = [num_1, num_2, num_3, num_4]

    def __eq__(self, other) -> bool:
        if isinstance(other, str):
            other = IPv4.from_str(other)
        
        if isinstance(other, list):
            if all(isinstance(ii, UInt8) for ii in other):
                other = IPv4.from_list(other)

            elif all(isinstance(ii, int) for ii in other):
                other = IPv4.from_raw_list(other)
                
            else:
                return False
        
        if isinstance(other, IPv4):
            return (
                self._address_list[0].v == other._address_list[0].v and
                self._address_list[1].v == other._address_list[1].v and
                self._address_list[2].v == other._address_list[2].v and 
                self._address_list[3].v == other._address_list[3].v
            )

        return False

    def __str__(self) -> str:
        return ".".join(str(ii.v) for ii in self._address_list)

    def as_list(self) -> List[UInt8]:
        return self._address_list

    def as_list_raw(self) -> List[int]:
        return [ii.v for ii in self._address_list]

    def is_localhost(self):
        return self._address_list == [127, 0, 0, 1]

    @staticmethod
    def from_str(raw_ip_addr: str) -> "Optional[IPv4]":
        if raw_ip_addr.lower() == "localhost":
            return IPv4.localhost()
        
        tokens = raw_ip_addr.split(".")
        if len(tokens) != 4:
            return None

        try:
            return IPv4(*[UInt8(int(ii), raise_if_out_of_bounds=True) for ii in tokens])
        except ValueError:
            return None

    @staticmethod
    def from_list(ip_addr_list: List[UInt8]) -> "Optional[IPv4]":
        if len(ip_addr_list) != 4:
            return None
        
        return IPv4(*ip_addr_list)
    
    @staticmethod
    def from_raw_list(raw_ip_addr_list: List[int]) -> "Optional[IPv4]":
        if len(raw_ip_addr_list) != 4:
            return None
        
        return IPv4(*[UInt8(ii) for ii in raw_ip_addr_list])

    @staticmethod
    def localhost() -> "IPv4":
        return IPv4(UInt8(127), UInt8(0), UInt8(0), UInt8(1))

    @staticmethod
    def default_netmask() -> "IPv4":
        return IPv4(UInt8(255), UInt8(255), UInt8(255), UInt8(0))


@dataclasses.dataclass
class IPv4WithPort:
    address: IPv4
    port: int

    def __str__(self):
        return f"{self.address}:{self.port}"
    
    def __eq__(self, other) -> bool:
        if isinstance(other, str):
            return str(self) == other
        if isinstance(other, list):
            if len(other) == 5:
                return self.address == other[0:4] and self.port == other[4]
            return False
        if isinstance(other, IPv4WithPort):
            return (
                self.address == other.address and
                self.port == other.port
            )
        return False
    
    def is_localhost(self):
        return self.address.is_localhost()
    
    def as_list_raw(self):
        return self.address.as_list_raw() + [self.port]

    @staticmethod
    def from_str(raw_ip_address_and_port: str) -> "Optional[IPv4WithPort]":
        tokens = raw_ip_address_and_port.split(":")
        if len(tokens) != 2:
            return None
        
        maybe_addr = IPv4.from_str(tokens[0])
        if maybe_addr is None:
            return None
        
        try:
            port = int(tokens[1])
        except ValueError:
            return None
        
        return IPv4WithPort(maybe_addr, port)
    
    @staticmethod
    def localhost(port: int) -> "IPv4WithPort":
        return IPv4WithPort(IPv4.localhost(), port)
    
    @staticmethod
    def localhost_any_port() -> "IPv4WithPort":
        return IPv4WithPort(IPv4.localhost(), 0)
    

class MacAddr48:
    def __init__(
        self, num_1: UInt8, num_2: UInt8, num_3: UInt8, num_4: UInt8, num_5: UInt8, num_6: UInt8
    ):
        self._address_list = [num_1, num_2, num_3, num_4, num_5, num_6]

    def __str__(self) -> str:
        return ":".join(hex(ii.v)[2:].upper().zfill(2) for ii in self._address_list)

    def __eq__(self, other) -> bool:
        if isinstance(other, str):
            other = MacAddr48.from_str(other)
        
        if isinstance(other, list):
            if all(isinstance(ii, UInt8) for ii in other):
                other = MacAddr48.from_list(other)

            elif all(isinstance(ii, int) for ii in other):
                other = MacAddr48.from_raw_list(other)
            else:
                return False
        
        if isinstance(other, MacAddr48):
            return (
                self._address_list[0].v == other._address_list[0].v and
                self._address_list[1].v == other._address_list[1].v and
                self._address_list[2].v == other._address_list[2].v and 
                self._address_list[3].v == other._address_list[3].v and
                self._address_list[4].v == other._address_list[4].v and
                self._address_list[5].v == other._address_list[5].v
            )
        return False

    def as_list(self) -> List[UInt8]:
        return self._address_list

    def as_list_raw(self) -> List[int]:
        return [ii.v for ii in self._address_list]

    @staticmethod
    def from_str(raw_mac_addr: str) -> "Optional[MacAddr48]":
        tokens = raw_mac_addr.upper().split(":")
        if len(tokens) != 6:
            tokens = raw_mac_addr.split("-")
            if len(tokens) != 6:
                return None

        try:
            return MacAddr48(
                *[UInt8(int(f"0x{ii}", base=16), raise_if_out_of_bounds=True) for ii in tokens]
            )
        except ValueError:
            return None

    @staticmethod
    def from_list(raw_mac_addr_list: List[UInt8]) -> "Optional[MacAddr48]":
        if len(raw_mac_addr_list) != 6:
            return None
        
        return MacAddr48(*raw_mac_addr_list)
    
    @staticmethod
    def from_raw_list(raw_mac_addr_list: List[int]) -> "Optional[MacAddr48]":
        if len(raw_mac_addr_list) != 6:
            return None
        
        try:
            return MacAddr48(*[UInt8(ii, raise_if_out_of_bounds=True) for ii in raw_mac_addr_list])
        except ValueError:
            return None