#!/usr/bin/env python3

from typing import List, Optional, Type
from ipaddress import IPv4Network, IPv6Network
from alterator_bindings import backend3
from alterator_bindings.backend3 import *

from alterator_bindings.backend3 import translate as _

from alterator_net_ng.config.builder import (
    InitialConfigBuilder,
)
from alterator_net_ng.config.common_types import DNSConfig, Hostname, Port, Route
from alterator_net_ng.config.config import VethPair
from alterator_net_ng.errors import InvalidAddressError, InvalidHostnameError
from alterator_net_ng.interfaces import *
from alterator_net_ng import subsystem
from alterator_net_ng.subsystem.subsystem import NetworkSubsystem
from alterator_net_ng import i18n
import logging

backend3.ALTERATOR_DEBUG = True
backend3.TEXTDOMAIN = "alterator-net-ng"


def setup_logging(is_debug: bool):
    level = logging.DEBUG if is_debug else logging.INFO
    logger = logging.getLogger("alterator-net-ng")
    logger.setLevel(level)

    log_path = "/tmp/alterator-net-ng.log"

    try:
        fd = os.open(
            log_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600
        )

        log_file_stream = os.fdopen(fd, "w")

        handler = logging.StreamHandler(stream=log_file_stream)

    except (OSError, PermissionError) as e:
        print(f"CRITICAL: Failed to open log file securely: {e}")
        return

    formatter = logging.Formatter("%(levelname)s:%(name)s: %(message)s")
    handler.setFormatter(formatter)

    logger.addHandler(handler)
    logger.debug("Logger started. File is truncated and secure.")


setup_logging(backend3.ALTERATOR_DEBUG)
log = logging.getLogger("alterator-net-ng")

i18n.set_translator(_)

INTERFACE_TYPES = {
    "ethernet": (_("Ethernet Device"), EthernetInterface),
    "wireless": (_("Wi-Fi"), WirelessInterface),
    "linux_bridge": (_("Linux Bridge"), BridgeInterface),
    "linux_bond": (_("Linux Bonding"), BondInterface),
    "vlan": (_("VLAN"), VlanInterface),
    "vxlan": (_("VXLAN"), VxlanInterface),
    "veth_pair": (_("Veth Pair"), VethPair),
    "veth": (_("Virtual Ethernet Device"), VethInterface),
    "dummy": (_("Dummy Interface"), DummyInterface),
}


def pretty_interface_type_name(kind: str):
    t = INTERFACE_TYPES.get(kind)
    if t is None:
        return _("Unknown")
    else:
        return _(t[0])


# Matches iface.iftype namings
ADDABLE_INTERFACE_TYPES = [
    "linux_bridge",
    "linux_bond",
    "vlan",
    "vxlan",
    "veth_pair",
    "dummy",
]

PREDEFINED_MTU_VALUES = {
    "None": (None, _("Not assigned")),
    "1500": (1500, _("1500 Bytes (Default)")),
    "1492": (1492, _("1492 Bytes (PPPoE)")),
    "9000": (9000, _("9000 Bytes (Jumbo Frames)")),
}

IP_FAMILY_TYPES = {
    "ipv4": _("IPv4"),
    "ipv6": _("IPv6"),
}


def parse_interface_name(message: dict, label: str = "iface_name") -> InterfaceName:
    return InterfaceName(message.get(label))


def parse_subsystem(message: dict) -> Type[NetworkSubsystem]:
    sys_name = message.get("subsystem")
    subsys = subsystem.get_available_subsystems().get(sys_name)
    if subsys is None:
        raise Exception(_("Network subsystem is not selected"))
    return subsys


def parse_hostname(message: dict) -> Hostname:
    return Hostname(message.get("hostname"))


class UIState:

    interface_type: Type[NetworkInterface]
    interface_type_name: str
    staged_purge_slaves_l3: bool = False
    initial_staged_name: InterfaceName
    staged_name: InterfaceName

    # Hardware related stuff (never edited by user, just to forward it to new interface instance)
    staged_mac: Optional[str]
    staged_product_name: Optional[str]
    staged_is_plugged: Optional[bool]

    # Common fields for all interfaces
    staged_address_list: Optional[List[Address]]
    staged_dhcp4: Optional[bool]
    staged_dhcp6: Optional[bool]
    staged_gateway: Optional[Address]
    staged_dns: Optional[DNSConfig]
    staged_mtu: Optional[int]
    staged_routes: Optional[List[Route]]

    # For Bond/Bridge
    staged_members: Optional[List[InterfaceName]]

    # For Bond
    staged_bonding_mode: Optional[BondingMode]
    staged_bonding_lacp_rate: Optional[LACPRate]
    staged_bonding_ad_select: Optional[AdSelect]
    staged_bonding_transmit_hash_policy: Optional[TransmitHashPolicy]

    # For Bridge
    staged_bridge_stp: Optional[bool]

    # Similar to hardware stuff (we won't allow editting Veth peer in UI, just remember it to forward)
    staged_veth_peer_name: Optional[InterfaceName]

    # For VLAN/VXLAN
    staged_parent: Optional[InterfaceName]

    # For VLAN
    staged_vlan_id: Optional[VlanID]

    # For VXLAN
    staged_vxlan_id: Optional[VNI]
    staged_vxlan_destination: Optional[Address]
    staged_vxlan_local: Optional[Address]
    staged_vxlan_port: Optional[Port]

    # For Wi-Fi
    staged_wireless_access_points: Optional[dict[SSID, Optional[WLANPassword]]]

    # IMPORTANT: maps this class fields names to names of args __init__ of interfaces classes
    # Always adapt this mapping to changes!
    _FIELD_MAPPING = {
        "staged_name": "name",
        "staged_mac": "mac",
        "staged_product_name": "product_name",
        "staged_is_plugged": "is_plugged",
        "staged_address_list": "addresses",
        "staged_dhcp4": "dhcp4",
        "staged_dhcp6": "dhcp6",
        "staged_gateway": "gateway",
        "staged_dns": "dns",
        "staged_mtu": "mtu",
        "staged_routes": "routes",
        "staged_members": "members",
        "staged_bonding_mode": "mode",
        "staged_bonding_lacp_rate": "lacp_rate",
        "staged_bonding_ad_select": "ad_select",
        "staged_bonding_transmit_hash_policy": "transmit_hash_policy",
        "staged_bridge_stp": "stp",
        "staged_parent": "parent",
        "staged_vlan_id": "vlan_id",
        "staged_vxlan_id": "vxlan_id",
        "staged_vxlan_destination": "destination",
        "staged_vxlan_local": "local",
        "staged_vxlan_port": "port",
        "staged_wireless_access_points": "access_points",
        "staged_veth_peer_name": "peer",
    }

    def __init__(self):
        self.reset_config()

    def reset_staged_values_to_none(self):
        self.initial_staged_name = None
        self.staged_name = None
        self.staged_purge_slaves_l3 = False

        for field_name in self._FIELD_MAPPING.keys():
            setattr(self, field_name, None)
        log.debug("Set every staged field to None")

    def stage_iface_for_edit(self, message: dict):
        iface_name = parse_interface_name(message)
        iface = self.config.get_interface(iface_name)

        self.reset_staged_values_to_none()

        self.initial_staged_name = iface_name
        self.staged_name = iface_name
        self.interface_type = type(iface)
        self.interface_type_name = iface.type

        # Common fields
        self.staged_address_list = iface.addresses
        self.staged_dhcp4 = iface.dhcp4
        self.staged_dhcp6 = iface.dhcp6
        self.staged_gateway = iface.gateway
        self.staged_dns = iface.dns
        self.staged_mtu = iface.mtu
        self.staged_routes = list(iface.routes) if iface.routes else []

        if isinstance(iface, BondInterface):
            self.staged_members = list(iface.members)
            self.staged_bonding_mode = iface.mode
            self.staged_bonding_lacp_rate = iface.lacp_rate
            self.staged_bonding_ad_select = iface.ad_select
            self.staged_bonding_transmit_hash_policy = iface.transmit_hash_policy
        elif isinstance(iface, BridgeInterface):
            self.staged_members = list(iface.members)
            self.staged_bridge_stp = iface.stp
        elif isinstance(iface, VlanInterface):
            self.staged_parent = iface.parent
            self.staged_vlan_id = iface.vlan_id
        elif isinstance(iface, VxlanInterface):
            self.staged_parent = iface.parent
            self.staged_vxlan_destination = iface.destination
            self.staged_vxlan_id = iface.vxlan_id
            self.staged_vxlan_local = iface.local
            self.staged_vxlan_port = iface.port
        elif isinstance(iface, WirelessInterface):
            self.staged_wireless_access_points = iface.access_points
            self.staged_mac = iface.mac
            self.staged_product_name = iface.product_name
        elif isinstance(iface, EthernetInterface):
            self.staged_is_plugged = iface.is_plugged
            self.staged_mac = iface.mac
            self.staged_product_name = iface.product_name
        elif isinstance(iface, (VethInterface)):
            self.staged_veth_peer_name = iface.peer
        elif isinstance(iface, DummyInterface):
            # No special fields
            pass
        else:
            raise Exception(
                f"Trying to stage interface {str(iface_name)} with unknown type for edit"
            )

        log.info("Staged interface %s for editting", str(iface_name))

    def stage_new_iface(self, message: dict):
        iftype = message.get("iface_type")

        if iftype not in ADDABLE_INTERFACE_TYPES:
            raise Exception(f"Interface of type {iftype} can't be created!")

        (_, iface_cls) = INTERFACE_TYPES.get(iftype)
        self.reset_staged_values_to_none()

        self.interface_type = iface_cls
        self.interface_type_name = iftype
        self.initial_staged_name = None
        self.staged_name = self.suggest_interface_name(self.interface_type)

        log.info("Staged creation of device of type %s", iftype)

    def _create_constructor_dict(self) -> dict:
        kwargs = {}
        for state_field, init_arg in self._FIELD_MAPPING.items():
            value = getattr(self, state_field, None)

            if value is not None:
                kwargs[init_arg] = value

        return kwargs

    def build_interface(self) -> NetworkInterface:
        constructor_kwargs = self._create_constructor_dict()

        return self.interface_type(**constructor_kwargs)

    def create_veth_pair(self, message: dict):
        veth1 = parse_interface_name(message, "veth1")
        veth2 = parse_interface_name(message, "veth2")

        self.config.add_interface(
            VethPair(
                VethInterface(name=veth1, peer=veth2),
                VethInterface(name=veth2, peer=veth1),
            )
        )
        self.reset_staged_values_to_none()

    # For everything except VethPair
    def commit_staged_interface(self):
        iface = self.build_interface()
        if self.initial_staged_name is None:
            # Creating new one
            self.config.add_interface(iface)
            log.info("Added new interface %s to config", str(self.staged_name))
        else:
            self.config.update_interface(self.initial_staged_name, iface)
            log.info(
                "Changed existing interface %s (new name is %s) in config",
                str(self.initial_staged_name),
                str(self.staged_name),
            )

        if self.staged_purge_slaves_l3:
            self._purge_slaves_l3_config(iface)

        self.reset_staged_values_to_none()

    def _purge_interface_l3_config(self, name: InterfaceName):
        iface = self.config.get_interface(name)
        iface.addresses = []
        iface.dns = None
        iface.dhcp4 = False
        iface.dhcp6 = False
        iface.gateway = None
        iface.routes = []

        self.config.update_interface(iface.name, iface)

        log.debug("L3 configuration of %s was purged", name)

    def _purge_slaves_l3_config(self, iface: NetworkInterface):
        for member_name in iface.members:
            self._purge_interface_l3_config(member_name)

        for sub_name in iface.subordinates:
            self._purge_interface_l3_config(sub_name)

    def delete_interface(self, message: dict):
        iface_name = parse_interface_name(message)
        self.config.remove_interface(iface_name)
        log.info("Deleted interface: %s", str(iface_name))

    def get_standardized_interface_type(self, message: dict):
        iface_name = parse_interface_name(message)
        write_string_param("iface_type", self.config.get_interface(iface_name).type)

    def suggest_interface_name(
        self, iface_type: Type[NetworkInterface]
    ) -> InterfaceName:
        # TODO: do similar for VLAN/VXLAN
        PREFIX = {
            BridgeInterface: "br",
            BondInterface: "bond",
            DummyInterface: "dummy",
        }

        prefix = PREFIX.get(iface_type)

        if not prefix:
            return None

        existing_names = {str(iface.name) for iface in self.config.interfaces_view()}

        i = 0
        while True:
            candidate = f"{prefix}{i}"
            if candidate not in existing_names:
                return InterfaceName(candidate)
            i += 1

    def suggest_veth_pair_names(self):
        existing_names = {str(iface.name) for iface in self.config.interfaces_view()}

        i = 0
        while True:
            candidate_a = f"veth{i}"
            candidate_b = f"veth{i + 1}"
            if candidate_a not in existing_names and candidate_b not in existing_names:
                write_string_param("first", candidate_a)
                write_string_param("second", candidate_b)
                return

            i += 1

    # General page methods

    def get_staged_interface_detailed_info(self):
        if self.staged_is_plugged is None:
            plugged = ""
        else:
            plugged = _("yes") if self.staged_is_plugged else _("no")

        write_string_param("model", self.staged_product_name or "")
        write_string_param("mac", self.staged_mac or "")
        write_string_param("plugged", plugged)
        write_string_param("peer", str(self.staged_veth_peer_name or ""))

    def get_staged_interface_name(self):
        if self.staged_name is None:
            iface_name = ""
        else:
            iface_name = str(self.staged_name)

        write_string_param("iface_name", iface_name)

    def set_staged_interface_name(self, message: dict):
        iface_name = parse_interface_name(message)
        self.staged_name = iface_name

        log.debug("Staged interface name: %s" % str(self.staged_name))

    def get_staged_interface_mtu(self):
        if self.staged_mtu is None:
            key = "None"
        else:
            key = str(self.staged_mtu)

        if key in PREDEFINED_MTU_VALUES:
            write_string_param("predefined", key)
            write_bool_param("manual", False)
        else:
            write_bool_param("predefined", False)
            write_string_param("manual", key)

    def set_staged_interface_mtu(self, message: dict):
        mtu = message.get("mtu")
        predefined = PREDEFINED_MTU_VALUES.get(mtu)
        if predefined is not None:
            self.staged_mtu = predefined[0]
        else:
            self.staged_mtu = int(mtu)

        log.debug("Staged interface MTU: %s" % str(self.staged_mtu))

    # Addresses page methods

    def get_staged_dhcp_setup(self):
        dhcp4 = self.staged_dhcp4 if self.staged_dhcp4 is not None else False
        dhcp6 = self.staged_dhcp6 if self.staged_dhcp6 is not None else False

        write_bool_param("dhcp4", dhcp4)
        write_bool_param("dhcp6", dhcp6)

    def set_staged_dhcp_setup(self, message: dict):
        self.staged_dhcp4 = test_bool(message.get("dhcp4"))
        self.staged_dhcp6 = test_bool(message.get("dhcp6"))

        log.debug(
            "Staged interface DHCP setup: DHCP4=%s, DHCP6=%s",
            self.staged_dhcp4,
            self.staged_dhcp6,
        )

    def get_staged_default_gateway(self):

        write_string_param(
            "gateway",
            self.staged_gateway.address if self.staged_gateway is not None else "",
        )

    def set_staged_default_gateway(self, message: dict):
        gateway = message.get("gateway")
        if gateway:
            try:
                self.staged_gateway = Address(gateway)
            except:
                raise InvalidAddressError(
                    _("Invalid default gateway address: '{gateway}'").format(
                        gateway=gateway
                    )
                )

        log.debug("Staged interface Default Gateway: %s", self.staged_gateway)

    def get_staged_dns_setup(self):
        nameservers_string = ""
        search_string = ""

        if self.staged_dns:
            nameservers_string = " ".join(
                ns.address for ns in self.staged_dns.nameservers
            )

            search_string = ""
            if self.staged_dns.search:
                search_string = " ".join(
                    str(domain) for domain in self.staged_dns.search
                )

        write_string_param("nameservers", nameservers_string)
        write_string_param("search", search_string)

    def set_staged_dns_setup(self, message: dict):
        nameservers_string: str = message.get("nameservers")
        search_string: str = message.get("search")

        nameservers = []
        search = []
        if nameservers_string:
            try:
                nameservers = [Address(ns) for ns in nameservers_string.split()]
            except InvalidAddressError as err:
                raise ValueError(
                    _("Bad nameserver address. {error}").format(error=str(err))
                )

        if search_string:
            try:
                search = [Hostname(domain) for domain in search_string.split()]
            except InvalidHostnameError as err:
                raise ValueError(_("Bad search domain. {error}").format(error=str(err)))

        self.staged_dns = DNSConfig(nameservers, search)

        log.debug("Staged interface DNS: %s", self.staged_dns)

    # Routes page methods

    def get_staged_routes_list(self):
        if not self.staged_routes:
            return

        for route in self.staged_routes:
            if route.is_default:
                to_str = _("default")
            else:
                to_str = str(route.to)

            write_table_item(
                {
                    "destination": to_str,
                    "gateway": str(route.via),
                    "metric": str(route.metric) if route.metric is not None else "",
                }
            )

    def add_route_to_list(self, message: dict):
        via_str = message.get("gateway")
        to_str = message.get("destination")
        metric_str = message.get("metric")

        if not via_str:
            raise ValueError(_("Gateway address is required"))

        try:
            via = Address(via_str)
        except:
            raise InvalidAddressError(
                _("Invalid gateway address: '{gateway}'").format(gateway=via_str)
            )

        to = None
        if to_str:
            try:
                parts = to_str.split("/")
                if len(parts) != 2:
                    raise ValueError(
                        _("Destination must include prefix length (e.g. 10.0.0.0/8)")
                    )
                to = Address(parts[0], int(parts[1]))
            except (InvalidAddressError, ValueError):
                raise InvalidAddressError(
                    _(
                        "Invalid destination: '{dest}'. Use CIDR notation (e.g. 10.0.0.0/8)"
                    ).format(dest=to_str)
                )

        metric = None
        if metric_str:
            try:
                metric = int(metric_str)
            except ValueError:
                raise ValueError(
                    _(
                        "Invalid metric value: '{metric}'. Must be a non-negative integer."
                    ).format(metric=metric_str)
                )

        route = Route(via=via, to=to, metric=metric)

        if self.staged_routes is None:
            self.staged_routes = []
        self.staged_routes.append(route)

        log.debug("Added route to list. Current list: %s", self.staged_routes)

    def delete_route_from_list(self, message: dict):
        index = message.get("index")
        if index is None or int(index) < 0 or int(index) >= len(self.staged_routes):
            raise KeyError('Invalid index in routes list: "%s"' % index)

        self.staged_routes.pop(int(index))
        log.debug("Removed route from list. Current list: %s", self.staged_routes)

    def log_ip_addresses_list(self):
        log.debug("Staged IP addresses list: %s", self.staged_address_list)

    def add_ip_address_to_list(self, message: dict):
        address = message.get("address")
        mask = message.get("mask")
        ip = Address(address=address, prefixlen=int(mask))
        if message.get("family") != ip.family:
            raise InvalidAddressError(
                _("Wrong IP version for address '{addr}'").format(addr=str(ip))
            )

        if self.staged_address_list is None:
            self.staged_address_list = []
        self.staged_address_list.append(ip)

        log.debug("Added new IP to list. Current list: %s", self.staged_address_list)

    def delete_ip_address_from_list(self, message: dict):
        index = message.get("index")
        if (
            index is None
            or int(index) < 0
            or int(index) >= len(self.staged_address_list)
        ):
            raise KeyError('Invalid index in IP list: "%s"' % index)

        self.staged_address_list.pop(int(index))
        log.debug("Removed IP from list. Current list: %s", self.staged_address_list)

    def get_ip_addresses_list(self):
        if not self.staged_address_list:
            return

        for address in self.staged_address_list:
            write_table_item(
                {"ipv": _(IP_FAMILY_TYPES[address.family]), "address": str(address)}
            )

    # Common for Bridge and Bond
    def get_staged_members(self):
        members_str = ""
        if self.staged_members is not None:
            members_str = ";".join(str(iface) for iface in self.staged_members)
        write_string_param("members", members_str)

    def set_staged_members(self, message: dict):
        members_str: str = message.get("members")
        if members_str:
            self.staged_members = [
                InterfaceName(member) for member in members_str.split(";")
            ]
        else:
            self.staged_members = []

        self.staged_purge_slaves_l3 = test_bool(message.get("purge_members"))

        log.debug(
            "Staged interface members: %s. Purge L3: %s",
            self.staged_members,
            self.staged_purge_slaves_l3,
        )

    # Bridge-specific page

    def get_bridge_member_candidates(self):
        for iface in self.config.get_free_bridgeable_interfaces(
            self.initial_staged_name
        ):
            write_enum_item(str(iface.name))

    def get_staged_bridge_stp(self):
        write_bool_param(
            "stp",
            self.staged_bridge_stp if self.staged_bridge_stp is not None else True,
        )

    def set_staged_bridge_stp(self, message: dict):
        self.staged_bridge_stp = test_bool(message.get("stp"))

        log.debug("Staged bridge STP: %s", self.staged_bridge_stp)

    # Bond specific page

    def get_bond_member_candidates(self):
        for iface in self.config.get_free_bondable_interfaces(self.initial_staged_name):
            write_enum_item(str(iface.name))

    def get_staged_bonding_parameters(self):
        if self.staged_bonding_mode is not None:
            write_string_param("bonding_mode", str(self.staged_bonding_mode))
        else:
            write_bool_param("bonding_mode", False)

        if self.staged_bonding_lacp_rate is not None:
            write_string_param("lacp_rate", str(self.staged_bonding_lacp_rate))
        else:
            write_bool_param("lacp_rate", False)

        if self.staged_bonding_ad_select is not None:
            write_string_param("ad_select", str(self.staged_bonding_ad_select))
        else:
            write_bool_param("ad_select", False)

        if self.staged_bonding_transmit_hash_policy is not None:
            write_string_param(
                "transmit_hash_policy", str(self.staged_bonding_transmit_hash_policy)
            )
        else:
            write_bool_param("transmit_hash_policy", False)

    def set_staged_bonding_parameters(self, message: dict):
        mode = BondingMode(message.get("bonding_mode"))
        transmit_hash_policy = None

        match mode:
            case BondingMode.BALANCE_XOR | BondingMode.LACP | BondingMode.BALANCE_TLB:
                transmit_hash_policy = TransmitHashPolicy(
                    message.get("transmit_hash_policy")
                )

        ad_select = None
        lacp_rate = None
        if mode == BondingMode.LACP:
            ad_select = AdSelect(message.get("ad_select"))
            lacp_rate = LACPRate(message.get("lacp_rate"))

        self.staged_bonding_mode = mode
        self.staged_bonding_transmit_hash_policy = transmit_hash_policy
        self.staged_bonding_lacp_rate = lacp_rate
        self.staged_bonding_ad_select = ad_select

        log.debug(
            "Staged bonding parameters. mode = %s, transmit_hash_policy = %s, lacp_rate = %s, ad_select = %s",
            self.staged_bonding_mode,
            self.staged_bonding_transmit_hash_policy,
            self.staged_bonding_lacp_rate,
            self.staged_bonding_ad_select,
        )

    # VLAN / VXLAN common
    def get_overlay_parent_candidates(self):
        for iface in self.config.interfaces_view():
            if iface.name != self.staged_name:
                write_enum_item(str(iface.name))

    def get_staged_parent(self):
        if self.staged_parent is not None:
            write_string_param("parent", str(self.staged_parent))
        else:
            # Just set to any suitable interface by default
            for iface in self.config.interfaces_view():
                if iface.name != self.staged_name:
                    write_string_param("parent", str(iface.name))
                    return

            write_bool_param("parent", False)

    def set_staged_parent(self, message: dict):
        parent = parse_interface_name(message, "parent")
        self.staged_parent = parent

        self.staged_purge_slaves_l3 = test_bool(message.get("purge_parent"))

        log.debug(
            "Staged interface parent: %s. Purge parent L3: %s",
            self.staged_parent,
            self.staged_purge_slaves_l3,
        )

    def should_derive_staged_name(self):
        derivable_type = self.interface_type in (VlanInterface, VxlanInterface)

        if not derivable_type:
            write_bool_param("derive", False)
            return

        if self.initial_staged_name is None:
            write_bool_param("derive", True)
            return

        if self.interface_type is VlanInterface:
            reference_name = InterfaceName(
                f"{self.staged_parent}.{self.staged_vlan_id}"
            )
        else:
            reference_name = InterfaceName(f"vxlan{self.staged_vxlan_id}")

        write_bool_param("derive", reference_name == self.initial_staged_name)

    # VLAN specific

    def get_staged_vlan_id(self):
        if self.staged_vlan_id is not None:
            write_string_param(
                "vlan_id",
                str(self.staged_vlan_id),
            )
        else:
            write_bool_param("vlan_id", False)

    def set_staged_vlan_id(self, message: dict):
        self.staged_vlan_id = VlanID(int(message.get("vlan_id")))

        log.debug("Staged VLAN ID: %s", self.staged_vlan_id)

    # VXLAN specific

    def get_staged_vni(self):
        if self.staged_vxlan_id is not None:
            write_string_param(
                "vni",
                str(self.staged_vxlan_id),
            )
        else:
            write_bool_param("vni", False)

    def set_staged_vni(self, message: dict):
        self.staged_vxlan_id = VNI(int(message.get("vni")))

        log.debug("Staged VXLAN ID: %s", self.staged_vlan_id)

    def get_staged_vxlan_destination(self):
        if self.staged_vxlan_destination is not None:
            write_string_param(
                "destination",
                str(self.staged_vxlan_destination),
            )
        else:
            write_string_param("destination", "239.1.1.1")

    def set_staged_vxlan_destination(self, message: dict):
        destination = message.get("destination")
        try:
            self.staged_vxlan_destination = Address(destination)
            log.debug("Staged VXLAN Destination: %s", self.staged_vxlan_destination)
        except:
            raise InvalidAddressError(
                _("Invalid Remote VTEP IP: '{remote}'").format(remote=destination)
            )

    def get_staged_vxlan_optional_params(self):
        if self.staged_vxlan_local is None and self.staged_vxlan_port is None:
            write_bool_param("optionals", False)
            return

        write_bool_param("optionals", True)
        write_string_param("port", str(self.staged_vxlan_port or 4789))
        write_string_param(
            "local",
            (
                str(self.staged_vxlan_local)
                if self.staged_vxlan_local is not None
                else ""
            ),
        )

    def get_staged_vxlan_local_address_opts(self, message: dict):
        write_enum_item("", _("Not assigned"))
        if message.get("parent"):
            parent = parse_interface_name(message, "parent")
            parent = self.config.get_interface(parent)
            for addr in parent.addresses:
                addr.prefixlen = None
                write_enum_item(str(addr))

    def set_staged_vxlan_local(self, message: dict):
        optionals = test_bool(message.get("optionals"))
        if not optionals:
            self.staged_vxlan_local = None
        else:
            local = message.get("local")
            if local:
                try:
                    self.staged_vxlan_local = Address(local)
                except:
                    raise InvalidAddressError(
                        _("Invalid Source IP: '{source}'").format(source=local)
                    )

        log.debug("Staged VXLAN Source: %s", self.staged_vxlan_local)

    def set_staged_vxlan_port(self, message: dict):
        optionals = test_bool(message.get("optionals"))
        if not optionals:
            self.staged_vxlan_port = None
        else:
            port = message.get("port")
            if port:
                self.staged_vxlan_port = Port(int(message.get("port")))

        log.debug("Staged VXLAN Port: %s", self.staged_vxlan_port)

    # Wireless specific

    def get_single_staged_wifi_network(self):
        ssid_str = ""
        password_str = ""
        if self.staged_wireless_access_points:
            ssid, password = next(iter(self.staged_wireless_access_points.items()))
            ssid_str = str(ssid)
            if password:
                password_str = str(password)

        write_string_param("ssid", ssid_str)
        write_string_param("password", password_str)

    def set_single_staged_wifi_network(self, message: dict):
        ssid = message.get("ssid")
        password_str = message.get("password")
        if ssid:
            ssid = SSID(ssid)
            password = None
            if password_str:
                password = WLANPassword(password_str)
            self.staged_wireless_access_points = {ssid: password}
        else:
            self.staged_wireless_access_points = {}

        for s, p in self.staged_wireless_access_points.items():
            log.debug(
                "Staged Wi-Fi Network with SSID '%s' and password length = %s", s, p
            )

    def reset_config(self):
        self.config = InitialConfigBuilder().build()
        self.reset_staged_values_to_none()

        log.info("Config was reset!")

    def apply_config_now(self, message: dict):
        cls = parse_subsystem(message)
        hostname = parse_hostname(message)
        self.config.set_subsystem(cls())
        self.config.hostname = hostname
        to_clean = list(subsystem.get_available_subsystems().values())
        self.config.apply_now(to_clean=to_clean)

        log.info("Applied config!")

    def write_config(self, message: dict):
        cls = parse_subsystem(message)
        hostname = parse_hostname(message)
        self.config.set_subsystem(cls())
        self.config.hostname = hostname
        to_clean = list(subsystem.get_available_subsystems().values())
        self.config.write(to_clean=to_clean)

        log.info("Written config files!")

    def get_current_subsystem(self):
        if self.config.subsystem is not None:
            write_string_param("subsystem", self.config.subsystem.short_name())
        else:
            write_bool_param("subsystem", False)

    def get_interface_description(self, message: dict):
        name = message.get("iface")
        desc = self.config.get_interface(name).description

        for label, content in desc:
            if label == _("Type"):
                content = pretty_interface_type_name(content)
            write_table_item({"label": label, "content": content})

    def read_interfaces_list(self):
        for iface in self.config.interfaces_view():
            write_table_item(
                {
                    "interface_name": str(iface.name),
                    "interface_type": pretty_interface_type_name(iface.kind),
                    "interface_notes": iface.details,
                }
            )

    def get_hostname(self):
        write_string_param("hostname", str(self.config.hostname))


def get_available_subsystems():
    for id, cls in subsystem.get_available_subsystems().items():
        write_enum_item(id, cls.pretty_name())


def get_addable_interfaces():
    for kind in ADDABLE_INTERFACE_TYPES:
        write_enum_item(kind, pretty_interface_type_name(kind))


def prefix_to_netmask(prefix: int, family: str) -> str:
    if family == "ipv6":
        return str(IPv6Network(f"::/{prefix}").netmask)
    return str(IPv4Network(f"0.0.0.0/{prefix}").netmask)


def available_mask_lens(message: dict):
    ipv = message.get("ipv")
    if ipv == "ipv4":
        masks = range(33)
    else:
        masks = range(129)

    for mask in masks:
        write_enum_item(str(mask), label=f"/{mask} ({prefix_to_netmask(mask, ipv)})")


def available_ip_types():
    for ver, label in IP_FAMILY_TYPES.items():
        write_enum_item(ver, _(label))


def get_default_mtu_options():
    for value, (_p, label) in PREDEFINED_MTU_VALUES.items():
        write_enum_item(value, _(label))


def get_bonding_policy_options():
    for mode in BondingMode:
        write_enum_item(str(mode), mode.pretty_str())


def get_transmit_hash_policy_options():
    for policy in TransmitHashPolicy:
        write_enum_item(str(policy), policy.pretty_str())


def get_ad_select_options():
    for ag in AdSelect:
        write_enum_item(str(ag), ag.pretty_str())


def get_lacp_rate_options():
    for rate in LACPRate:
        write_enum_item(str(rate), rate.pretty_str())


state = UIState()


def message_handler(message: dict):
    action = message.get("action")
    object = message.get("_objects")

    if action == "list":
        match object:
            case "read_interfaces_list":
                state.read_interfaces_list()

            case "get_available_subsystems":
                get_available_subsystems()

            case "get_addable_interfaces":
                get_addable_interfaces()

            case "available_ip_types":
                available_ip_types()

            case "available_mask_lens":
                available_mask_lens(message)

            case "get_default_mtu_options":
                get_default_mtu_options()

            case "get_ip_addresses_list":
                state.get_ip_addresses_list()

            case "get_staged_routes_list":
                state.get_staged_routes_list()

            case "get_bridge_member_candidates":
                state.get_bridge_member_candidates()

            case "get_bond_member_candidates":
                state.get_bond_member_candidates()

            case "get_bonding_policy_options":
                get_bonding_policy_options()
            case "get_transmit_hash_policy_options":
                get_transmit_hash_policy_options()
            case "get_ad_select_options":
                get_ad_select_options()
            case "get_lacp_rate_options":
                get_lacp_rate_options()

            case "get_overlay_parent_candidates":
                state.get_overlay_parent_candidates()

            case "get_staged_vxlan_local_address_opts":
                state.get_staged_vxlan_local_address_opts(message)

    elif action == "read":
        match object:
            case "get_current_subsystem":
                state.get_current_subsystem()

            case "get_interface_description":
                state.get_interface_description(message)

            case "get_hostname":
                state.get_hostname()

            case "get_standardized_interface_type":
                state.get_standardized_interface_type(message)

            case "get_staged_interface_detailed_info":
                state.get_staged_interface_detailed_info()

            case "get_staged_interface_mtu":
                state.get_staged_interface_mtu()

            case "get_staged_interface_name":
                state.get_staged_interface_name()

            case "get_staged_dhcp_setup":
                state.get_staged_dhcp_setup()
            case "get_staged_dns_setup":
                state.get_staged_dns_setup()
            case "get_staged_default_gateway":
                state.get_staged_default_gateway()

            case "get_staged_bridge_stp":
                state.get_staged_bridge_stp()

            case "get_staged_bonding_parameters":
                state.get_staged_bonding_parameters()

            case "get_staged_members":
                state.get_staged_members()

            case "suggest_veth_pair_names":
                state.suggest_veth_pair_names()

            case "get_staged_parent":
                state.get_staged_parent()

            case "should_derive_staged_name":
                state.should_derive_staged_name()

            case "get_staged_vlan_id":
                state.get_staged_vlan_id()

            case "get_staged_vni":
                state.get_staged_vni()
            case "get_staged_vxlan_destination":
                state.get_staged_vxlan_destination()
            case "get_staged_vxlan_optional_params":
                state.get_staged_vxlan_optional_params()

            case "get_single_staged_wifi_network":
                state.get_single_staged_wifi_network()

    elif action == "write":
        match object:
            case "stage_iface_for_edit":
                state.stage_iface_for_edit(message)
            case "stage_new_iface":
                state.stage_new_iface(message)

            case "delete_interface":
                state.delete_interface(message)

            case "commit_general_settings":
                state.set_staged_interface_name(message)
                state.set_staged_interface_mtu(message)

            case "commit_addresses_settings":
                state.log_ip_addresses_list()
                state.set_staged_dhcp_setup(message)
                state.set_staged_dns_setup(message)
                state.set_staged_default_gateway(message)
            case "add_ip_address_to_list":
                state.add_ip_address_to_list(message)
            case "delete_ip_address_from_list":
                state.delete_ip_address_from_list(message)

            case "add_route_to_list":
                state.add_route_to_list(message)
            case "delete_route_from_list":
                state.delete_route_from_list(message)

            case "commit_bridge_settings":
                state.set_staged_bridge_stp(message)
                state.set_staged_members(message)

            case "commit_bonding_settings":
                state.set_staged_bonding_parameters(message)
                state.set_staged_members(message)

            case "commit_vlan_settings":
                state.set_staged_vlan_id(message)
                state.set_staged_parent(message)

            case "commit_vxlan_settings":
                state.set_staged_vni(message)
                state.set_staged_parent(message)
                state.set_staged_vxlan_destination(message)
                state.set_staged_vxlan_local(message)
                state.set_staged_vxlan_port(message)

            case "commit_wifi_settings":
                state.set_single_staged_wifi_network(message)

            case "create_veth_pair":
                state.create_veth_pair(message)

            case "commit_staged_interface":
                state.commit_staged_interface()

            case "reset_config":
                state.reset_config()
            case "apply_config_now":
                state.apply_config_now(message)
            case "write_config":
                state.write_config(message)


def on_message(message: dict):
    try:
        message_handler(message)
    except Exception as e:
        write_error(str(e))
        return


message_loop(on_message)
