"""OS-level system checks and functions for the comms system."""
from functools import lru_cache
from feathersdk.utils.feathertypes import *
import json
import os
import subprocess
import getpass
import socket
import struct


_SUDO_PASSWORD = None

# Default timeout in seconds for system commands to prevent hanging.
DEFAULT_TIMEOUT = 5.0


def is_can_interface(interface: str) -> bool:
    """Check if an interface is a CAN/vCAN interface."""
    return interface.startswith(("can", "vcan"))


def is_running_as_sudo() -> bool:
    """Returns True if the current process is running as sudo, False otherwise."""
    return os.geteuid() == 0


def run_system_command(cmd: List[str], allow_error: bool = False, timeout: Optional[float] = DEFAULT_TIMEOUT) -> subprocess.CompletedProcess:
    """Run a system command and check the result. Fails if the command returns a non-zero exit code."""
    # Get sudo password and send it into the command via stdin if needed
    stdin_input = None
    if cmd[0] == "sudo" and not is_running_as_sudo():
        global _SUDO_PASSWORD
        if _SUDO_PASSWORD is None:
            _SUDO_PASSWORD = getpass.getpass("Enter sudo password: ")
        if cmd[1] != "-S":
            cmd.insert(1, "-S")
        stdin_input = _SUDO_PASSWORD + "\n"

    result = subprocess.run(cmd, capture_output=True, text=True, input=stdin_input, timeout=timeout)
    if result.returncode != 0 and not allow_error:
        raise ValueError(f"Command \"{' '.join(cmd)}\" failed with return code {result.returncode}. " \
                         f"\n\tstdout: {result.stdout}, \n\tstderr: {result.stderr}")
    return result


def get_can_links_json() -> List[Dict[str, Any]]:
    """Return a list of JSON records for interfaces whose names start with 'can' or 'vcan'."""
    data = json.loads(run_system_command(["ip", "-details", "-json", "link"]).stdout)
    return [link for link in data if is_can_interface(link.get("ifname", ""))]


def get_all_physical_can_interfaces() -> List[str]:
    """Return a list of all physical CAN interfaces."""
    return [link["ifname"] for link in get_can_links_json() if link["ifname"].startswith("can")]


def enable_can_interface(interface: Union[str, List[str]], bitrate: int = 1_000_000) -> None:
    """Enable a CAN interface or multipleinterfaces.
    
    For a "can" interface:
    - If it doesn't exist, raise an error
    - If it exists but is down, enable it
    - If it exists and is up, make sure the bitrate is correct. Raise an error if it is not.

    For a "vcan" interface:
    - If it doesn't exist, create it
    - If it exists but is down, enable it
    - Bitrate is ignored
    """
    if isinstance(interface, (list, tuple)):
        for iface in interface:
            enable_can_interface(iface, bitrate)
        return
    
    _enable_can_helper(interface, bitrate)
    if not _enable_can_helper(interface, bitrate, just_checking=True):
        raise ValueError(f"Can interface {interface} failed to enable after running commands")


def get_iface_uid(interface: str) -> int:
    """Return the unique identifier of an interface.
    
    For CAN/vCAN interfaces, returns the unique identifier of the interface (which will change even if the interface is
    deleted and recreated with the same name). For ip addresses, returns integer value created by removing all '.'s and
    the ':' from the ip address.
    """
    if is_can_interface(interface):
        return socket.if_nametoindex(interface)
    else:
        return int(interface.replace(".", "").replace(":", ""))


def _get_can_bitrate(link: Dict[str, Any]) -> int:
    """Return the bitrate of a CAN interface"""
    ret = link['linkinfo']['info_data']['bittiming']['bitrate']
    if not isinstance(ret, int):
        raise ValueError(f"Can interface {link['ifname']} has invalid bitrate: {ret}")
    return ret


def _enable_can_helper(
    interface: str, 
    bitrate: int,
    can_links_json: Optional[List[Dict[str, Any]]] = None,
    just_checking: bool = False
) -> bool:
    """Helper function to enable a CAN interface. Returns True if the interface was already up, False otherwise."""
    can_links_json = get_can_links_json() if can_links_json is None else can_links_json
    
    for link in can_links_json:
        if link["ifname"] == interface:
            # The interface exists, check if it's up and ready. vCANs are always "UNKNOWN" state once created
            if link["operstate"] == "UP" or (interface.startswith("vcan") and link["operstate"] == "UNKNOWN"):
                if not interface.startswith("vcan") and _get_can_bitrate(link) != bitrate:
                    exp_str = f"Expected {bitrate}, got {_get_can_bitrate(link)}"
                    raise ValueError(f"Can interface {interface} is already up but has the wrong bitrate. {exp_str}")
                return True
            else:
                if just_checking:
                    return False
                _enable_can_interface(interface, bitrate)
            break
    else:
        # The interface doesn't exist. If it's a vCAN interface, create it. Otherwise, raise an error.
        if interface.startswith("vcan"):
            if just_checking:
                return False

            run_system_command(["sudo", "ip", "link", "add", "dev", interface, "type", "vcan"])
            _enable_can_interface(interface, bitrate)

            return False
        
        raise ValueError(f"Can interface {interface} not found")
    
    return False


def _enable_can_interface(interface: str, bitrate: int) -> None:
    """Helper function to enable a CAN/vCAN interface. Assumes the interface exists but is not up."""
    if not interface.startswith("vcan"):
        run_system_command(["sudo", "ip", "link", "set", interface, "type", "can", "bitrate", str(bitrate)])
    run_system_command(["sudo", "ip", "link", "set", interface, "up"])


def is_can_enabled(interface: str, bitrate: int = 1_000_000) -> bool:
    """Returns True if a CAN interface is enabled and the bitrate is correct, False otherwise."""
    return _enable_can_helper(interface, bitrate, just_checking=True)


def remove_all_vcan_interfaces(timeout: float = 5.0) -> None:
    """Removes all vCAN interfaces."""
    for iface in get_can_links_json():
        if iface["ifname"].startswith("vcan"):
            try:
                run_system_command(["sudo", "ip", "link", "delete", iface["ifname"]], allow_error=True, timeout=timeout)
            except Exception as e:
                print(f"Error removing vCAN interface {iface['ifname']}: {e}")


def bring_all_can_interfaces_down(timeout: float = 5.0) -> None:
    """Brings all CAN interfaces down."""
    for iface in get_all_physical_can_interfaces():
        try:
            run_system_command(["sudo", "ip", "link", "set", iface, "down"], allow_error=True, timeout=timeout)
        except Exception as e:
            print(f"Error bringing down CAN interface {iface}: {e}")


@lru_cache(maxsize=1)
def maybe_enable_all_can_interfaces() -> List[str]:
    """Enable all CAN interfaces. This function is only run once per process and memoized."""
    enabled_ifaces: List[str] = []
    for iface in get_all_physical_can_interfaces():
        try:
            enable_can_interface(iface, bitrate=1_000_000)
            enabled_ifaces.append(iface)
        except Exception as e:
            pass
    return enabled_ifaces


class FloatRange(NamedTuple):
    """A tuple of (min, max) float values."""
    min: float
    """The minimum value of the range."""
    max: float
    """The maximum value of the range."""

    def clamp(self, value: float) -> float:
        """Clamp a value to the range."""
        return max(self.min, min(self.max, value))
    
    def is_within_range(self, value: float) -> bool:
        """Check if a value is within the range."""
        return self.min <= value <= self.max


class StructType(str):
    """A struct format string type that can be used directly with struct.unpack() and has a .size attribute.
    
    For 0x87654321, little endian would look like: [0x21, 0x43, 0x65, 0x87], while big is: [0x87, 0x65, 0x43, 0x21].
    """
    _UNSIGNED_INT_FORMATS = {'B', 'H', 'I', 'Q', 'L'}
    _SIGNED_INT_FORMATS = {'b', 'h', 'i', 'q', 'l'}
    _FLOAT_FORMATS = {'f', 'd'}

    size: int
    """The size of the data type in bytes."""

    is_integer: bool
    """True if the data type is an integer, False otherwise."""

    is_signed: bool
    """True if the data type is a signed integer, False otherwise."""

    is_float: bool
    """True if the data type is a float, False otherwise."""

    is_bool: bool
    """True if the data type is a boolean, False otherwise."""

    is_char: bool
    """True if the data type is a character, False otherwise."""

    int_min: Optional[int]
    """The minimum value of the data type. None if the data type is not an integer."""

    int_max: Optional[int]
    """The maximum value of the data type. None if the data type is not an integer."""

    def __new__(cls, format_str: str) -> "StructType":
        instance = super().__new__(cls, format_str)
        instance.size = struct.calcsize(format_str)
        instance.is_integer = format_str[-1] in (cls._UNSIGNED_INT_FORMATS | cls._SIGNED_INT_FORMATS)
        instance.is_signed = format_str[-1] in (cls._SIGNED_INT_FORMATS | cls._FLOAT_FORMATS)
        instance.is_float = format_str[-1] in cls._FLOAT_FORMATS
        instance.is_bool = format_str == "?"
        instance.is_char = format_str == "c"

        if instance.is_integer or instance.is_char:
            bits = instance.size * 8
            instance.int_min = -(2 ** (bits - 1)) if instance.is_signed else 0
            instance.int_max = (2 ** (bits - 1)) - 1 if instance.is_signed else (2 ** bits) - 1
        elif instance.is_bool:
            instance.int_min = 0
            instance.int_max = 1
        else:
            instance.int_min = None
            instance.int_max = None
        
        return instance
    
    def unpack(self, data: Sequence[int]) -> Union[int, float, bool, str]:
        """Unpack a value from bytes. If len(data) > self.size, only use the first self.size bytes."""
        return struct.unpack(self, data[:self.size])[0]
    
    def pack(self, value: Union[int, float, bool, str], pad_end: Optional[int] = None, pad_val: int = 0) -> bytes:
        """Pack a value into bytes. 
        
        If pad_end is not None, pad the end of the bytes with pad_val until the length is at least pad_end.
        """
        ret = struct.pack(self, value)
        if pad_end is not None and len(ret) < pad_end:
            ret += bytes([pad_val] * (pad_end - len(ret)))
        return ret
    
    def _validate_int_range(self, val: Union[float, int], _min: Union[float, int], _max: Union[float, int]) -> None:
        """Validate that a value is within the given range. Only works for int types."""
        if not self.is_integer:
            raise ValueError(f"Cannot validate non-integer type \"{self}\"")
        if val < _min or val > _max:
            raise ValueError(f"Value {val} is out of bounds [{_min}, {_max}]")
    
    def unpack_and_map(self, data: Sequence[int], min_val: Union[float, int], max_val: Union[float, int]) -> float:
        """Unpack a value from bytes and map it to the correct range based on type limits. Doesn't do error checking.
        
        Assumes linear mapping from int_min -> int_max to min_val -> max_val. Only works for int types.
        """
        self._validate_int_range(val=min_val, _min=min_val, _max=max_val)
        if not isinstance(data, bytes):
            data = bytes(data)
        return ((self.unpack(data) - self.int_min) / (self.int_max - self.int_min)) * (max_val - min_val) + min_val
    
    def map_and_pack(self, value: Union[float, int], min_val: Union[float, int], max_val: Union[float, int]) -> bytes:
        """Map a value to the given range and pack it into bytes. Doesn't do error checking.
        
        Assumes linear mapping from min_val -> max_val to int_min -> int_max. Only works for int types.
        """
        self._validate_int_range(val=value, _min=min_val, _max=max_val)
        return self.pack(int((value - min_val) * (self.int_max - self.int_min) / (max_val - min_val) + self.int_min))
    
    @property
    def range(self) -> FloatRange:
        """The range of the data type as float values."""
        if self.is_integer or self.is_char:
            return FloatRange(self.int_min, self.int_max)
        elif self.is_float:
            return FloatRange(float('-inf'), float('inf'))
        elif self.is_bool:
            return FloatRange(0.0, 1.0)
        else:
            raise NotImplementedError(f"Unknown type for range calculation: \"{self}\"")


# Unsigned integers - Little endian
S_uint8le = StructType("<B")   # 1 byte
S_uint16le = StructType("<H")  # 2 bytes
S_uint32le = StructType("<I")  # 4 bytes
S_uint64le = StructType("<Q")  # 8 bytes

# Unsigned integers - Big endian
S_uint8be = StructType(">B")   # 1 byte
S_uint16be = StructType(">H")  # 2 bytes
S_uint32be = StructType(">I")  # 4 bytes
S_uint64be = StructType(">Q")  # 8 bytes

# Signed integers - Little endian
S_int8le = StructType("<b")   # 1 byte
S_int16le = StructType("<h")  # 2 bytes
S_int32le = StructType("<i")  # 4 bytes
S_int64le = StructType("<q")  # 8 bytes

# Signed integers - Big endian
S_int8be = StructType(">b")   # 1 byte
S_int16be = StructType(">h")  # 2 bytes
S_int32be = StructType(">i")  # 4 bytes
S_int64be = StructType(">q")  # 8 bytes

# Floating point - Little endian
S_float32le = StructType("<f")  # 4 bytes (float)
S_float64le = StructType("<d")  # 8 bytes (double)

# Floating point - Big endian
S_float32be = StructType(">f")  # 4 bytes (float)
S_float64be = StructType(">d")  # 8 bytes (double)

# Other types (endianness doesn't apply)
S_bool = StructType("?")  # 1 byte
S_char = StructType("c")  # 1 byte
