import json
from snappi_ixnetwork.timer import Timer
from snappi_ixnetwork.exceptions import SnappiIxnException


class Lag(object):
    """Transforms OpenAPI objects into IxNetwork objects
    - Lag to /lag
    Args
    ----
    - ixnetworkapi (Api): instance of the Api class
    """

    """
    These are supported keys to defined field (_ETHERNET/ _VLAN/ _LACP/ new one...)
    <stack_name> = {
        <key> : {
            "ixn_attr" : name in IxNetwork SDM
            "default" : default IxNetwork value. Consider snappi default already taken care.
            "translate" : translate snappi value to ixnetwork value help of self method 
            "enum_map" : enum map with snappi with ixnetwork
        }
    }
    """

    _ETHERNET = {
        "mac": {"ixn_attr": "mac", "default": "_default_mac"},
        "mtu": {"ixn_attr": "mtu", "default": "1500"},
    }

    _VLAN_TPID = {
        "x8100": "ethertype8100",
        "x88a8": "ethertype88a8",
        "x9100": "ethertype9100",
        "x9200": "ethertype9200",
        "x9300": "ethertype9300",
    }

    _VLAN = {
        # 'tpid' : {
        #     'ixn_attr' : 'tpid',
        #     'default' : 'ethertype8100',
        #     'enum_map' : _VLAN_TPID
        # },
        "priority": {"ixn_attr": "priority", "default": "0"},
        "vlanId": {"ixn_attr": "vlanId", "default": "1"},
    }

    _LACP = {
        "actor_key": {"ixn_attr": "actorKey", "default": "1"},
        "actor_system_id": {
            "ixn_attr": "actorSystemId",
            "default": "00 00 00 00 00 01",
            "translate": "_translate_actor_system_id",
        },
        "actor_system_priority": {
            "ixn_attr": "actorSystemPriority",
            "default": "1",
        },
    }

    _LACP_PORT_PROTOCOL = {
        "actor_port_number": {"ixn_attr": "actorPortNumber", "default": "1"},
        "actor_port_priority": {
            "ixn_attr": "actorPortPriority",
            "default": "1",
        },
        "lacpdu_periodic_time_interval": {
            "ixn_attr": "lacpduPeriodicTimeInterval",
            "default": "0",
        },
        "lacpdu_timeout": {"ixn_attr": "lacpduTimeout", "default": "0"},
        "actor_activity": {"ixn_attr": "lacpActivity", "default": "active"},
    }

    _STATIC = {"lag_id": {"ixn_attr": "lagId", "default": "1"}}

    def __init__(self, ixnetworkapi):
        self._api = ixnetworkapi
        self._lag_ports = {}

    def config(self):
        """Transform config.ports into Ixnetwork.Vport
        1) delete any vport that is not part of the config
        2) create a vport for every config.ports[] that is not present in IxNetwork
        3) set config.ports[].location to /vport -location using resourcemanager
        4) set /vport/l1Config/... properties using the corrected /vport -type
        5) connectPorts to use new l1Config settings and clearownership
        """
        self._resource_manager = self._api._ixnetwork.ResourceManager
        self._ixn_lag = self._api._lag
        self._lag_ports = {}
        self._lags_config = self._api.snappi_config.lags
        with Timer(self._api, "Lag Configuration"):
            self._delete_lags()
            if len(self._lags_config) == 0:
                return
            self._create_lags()
        with Timer(self._api, "Lag Ethernet Configuration"):
            self._ethernet_config()
        with Timer(self._api, "Lag Protocol Configuration"):
            self._protocol_config()

    def _import(self, imports):
        if len(imports) > 0:
            errata = self._resource_manager.ImportConfig(
                json.dumps(imports), False
            )
            for item in errata:
                self._api.warning(item)
            return len(errata) == 0
        return True

    def _delete_lags(self):
        """Delete any Lags from the api server that do not exist in the new config"""
        self._api._remove(self._ixn_lag, self._lags_config)

    def _select_lags(self):
        payload = {
            "selects": [
                {
                    "from": "/",
                    "properties": [],
                    "children": [
                        {"child": "lag", "properties": ["name"], "filters": []}
                    ],
                    "inlines": [],
                }
            ]
        }
        url = "%s/operations/select?xpath=true" % self._api._ixnetwork.href
        results = self._api._ixnetwork._connection._execute(url, payload)
        lags = {}
        if "lag" in results[0]:
            for lag in results[0]["lag"]:
                lags[lag["name"]] = lag
        return lags

    def _select_vports(self):
        """Select all vports.
        Return them in a dict keyed by vport name.
        """
        payload = {
            "selects": [
                {
                    "from": "/",
                    "properties": [],
                    "children": [
                        {
                            "child": "vport",
                            "properties": [
                                "name",
                            ],
                            "filters": [],
                        }
                    ],
                    "inlines": [],
                }
            ]
        }
        url = "%s/operations/select?xpath=true" % self._api._ixnetwork.href
        results = self._api._ixnetwork._connection._execute(url, payload)
        vports = {}
        if "vport" in results[0]:
            for vport in results[0]["vport"]:
                vports[vport["name"]] = vport
        return vports

    def _get_vports(self, ixn_vports, ports):
        vports = []
        for port in ports:
            if port.port_name not in ixn_vports:
                raise Exception("Port {0} is not available".format(port.name))
            vports.append(ixn_vports[port.port_name]["xpath"])
        return vports

    def _create_lags(self):
        """Add any Lags to the api server that do not already exist"""
        self._ixn_lag.find()
        existing_lags = [ixn_lag.Name for ixn_lag in self._ixn_lag]
        for lag in self._lags_config:
            self._lag_ports[lag.name] = lag.ports
            if lag.name not in existing_lags:
                self._ixn_lag.add(Name=lag.name)
        imports = []
        ixn_vports = self._select_vports()
        ixn_lags = self._select_lags()
        for name, ixn_lag in ixn_lags.items():
            self._api.ixn_objects.set(name, ixn_lag)
            lag_import = {
                "xpath": ixn_lag["xpath"],
                "vports": self._get_vports(ixn_vports, self._lag_ports[name]),
            }
            imports.append(lag_import)
        self._import(imports)

    def _set_multivalue(self, source, attribute, value):
        xpath = "/multivalue[@source = '{0} {1}']".format(source, attribute)
        if isinstance(value, list):
            return {"xpath": "{0}/valueList".format(xpath), "values": value}
        else:
            return {"xpath": "{0}/singleValue".format(xpath), "value": value}

    def _ethernet_config(self):
        imports = []
        ixn_lags = self._select_lags()
        for name, ports in self._lag_ports.items():
            ether_xpath = "{0}/protocolStack/ethernet[1]".format(
                ixn_lags[name]["xpath"]
            )
            ixn_lag = self._ixn_lag.find(
                Name="^%s$" % self._api.special_char(name)
            )
            ixn_proto_stack = ixn_lag.ProtocolStack.find()
            if len(ixn_proto_stack) == 0:
                ixn_proto_stack = ixn_lag.ProtocolStack.add()[0]
            ixn_proto_stack.Multiplier = 1
            for eth_attr in Lag._ETHERNET:
                attr_values = self._configure_attribute(
                    eth_attr, Lag._ETHERNET, ports, "ethernet"
                )
                imports.append(
                    self._set_multivalue(
                        ether_xpath,
                        attr_values.ixn_attribute,
                        attr_values.config_value,
                    )
                )
            vlans = self._process_vlans(ports)
            if len(vlans) > 0:
                if len(vlans) > 0:
                    imports.append(
                        self._set_multivalue(ether_xpath, "enableVlans", True)
                    )
                    imports.append(
                        {"xpath": ether_xpath, "vlanCount": len(vlans)}
                    )

                for idx, vlan in enumerate(vlans):
                    for vlan_attr in Lag._VLAN:
                        attr_values = self._configure_attribute(
                            vlan_attr, Lag._VLAN, vlan
                        )
                        vlan_xpath = "{0}/vlan[{1}]".format(
                            ether_xpath, idx + 1
                        )
                        imports.append(
                            self._set_multivalue(
                                vlan_xpath,
                                attr_values.ixn_attribute,
                                attr_values.config_value,
                            )
                        )
        self._import(imports)

    def _lacp_ports_config(self, name, ports):
        imports = []
        ixn_lags = self._select_lags()
        ixn_lag = self._ixn_lag.find(
            Name="^%s$" % self._api.special_char(name)
        )
        ixn_eth = ixn_lag.ProtocolStack.find().Ethernet.find()
        ixn_static = ixn_eth.Lagportstaticlag.find()
        lacp_port_protocols = []
        for port in ports:
            lacp_port_protocol = port.lacp
            lacp_port_protocols.append(lacp_port_protocol)
        if len(ixn_static) > 0:
            ixn_static.remove()
        lacp_xpath = "{0}/protocolStack/ethernet[1]/lagportlacp[1]".format(
            ixn_lags[name]["xpath"]
        )
        imports.append(self._set_multivalue(lacp_xpath, "active", True))
        for lacp_attr in Lag._LACP_PORT_PROTOCOL:
            attr_values = self._configure_attribute(
                lacp_attr, Lag._LACP_PORT_PROTOCOL, lacp_port_protocols
            )
            imports.append(
                self._set_multivalue(
                    lacp_xpath,
                    attr_values.ixn_attribute,
                    attr_values.config_value,
                )
            )
        return imports

    def _protocol_config(self):
        imports = []
        ixn_lags = self._select_lags()
        for snappi_lag in self._lags_config:
            ixn_lag = self._ixn_lag.find(
                Name="^%s$" % self._api.special_char(snappi_lag.name)
            )
            ixn_eth = ixn_lag.ProtocolStack.find().Ethernet.find()
            ixn_lacp = ixn_eth.Lagportlacp.find()
            ixn_static = ixn_eth.Lagportstaticlag.find()
            choice = None
            protocols = []
            protocol = snappi_lag.protocol
            if choice is None:
                choice = protocol.choice
            protocols.append(protocol)
            if choice is None:
                if len(ixn_static) > 0:
                    ixn_static.Active.Single(False)
                if len(ixn_lacp) > 0:
                    ixn_lacp.Active.Single(False)
                return
            if choice == "lacp":
                if len(ixn_static) > 0:
                    ixn_static.remove()
                lacp_xpath = (
                    "{0}/protocolStack/ethernet[1]/lagportlacp[1]".format(
                        ixn_lags[snappi_lag.name]["xpath"]
                    )
                )
                imports.append(
                    self._set_multivalue(lacp_xpath, "active", True)
                )
                for lacp_attr in Lag._LACP:
                    attr_values = self._configure_attribute(
                        lacp_attr, Lag._LACP, protocols, choice
                    )
                    imports.append(
                        self._set_multivalue(
                            lacp_xpath,
                            attr_values.ixn_attribute,
                            attr_values.config_value,
                        )
                    )
                lacp_port_imports = self._lacp_ports_config(
                    snappi_lag.name, snappi_lag.ports
                )
                imports += lacp_port_imports
            else:
                if len(ixn_lacp) > 0:
                    ixn_lacp.remove()
                static_xpath = (
                    "{0}/protocolStack/ethernet[1]/lagportstaticlag[1]".format(
                        ixn_lags[snappi_lag.name]["xpath"]
                    )
                )
                imports.append(
                    self._set_multivalue(static_xpath, "active", True)
                )
                for static_attr in Lag._STATIC:
                    attr_values = self._configure_attribute(
                        static_attr, Lag._STATIC, protocols, choice
                    )
                    imports.append(
                        self._set_multivalue(
                            static_xpath,
                            attr_values.ixn_attribute,
                            attr_values.config_value,
                        )
                    )

        self._import(imports)

    def _process_vlans(self, ports):
        vlan_count = -1
        vlan_list = []
        for port in ports:
            vlans = port.ethernet.vlans
            if vlan_count == -1:
                vlan_count = len(vlans)
                for i in range(len(vlans)):
                    vlan_list.append([vlans[i]])
            elif vlan_count == len(vlans):
                for i in range(len(vlans)):
                    vlan_list[i].append(vlans[i])
            else:
                raise Exception("Please configure equal numbers of VLANs")
        return vlan_list

    def _configure_attribute(self, attr, mapper, parent_list, obj_name=None):
        attr_values = ProtocolAttributes()
        attr_mapper = mapper[attr]
        ixn_attr = attr_mapper["ixn_attr"]
        default_value = attr_mapper["default"]
        enum_map = attr_mapper.get("enum_map")
        translate = attr_mapper.get("translate")
        default_obj = getattr(self, default_value, None)
        attr_values.ixn_attribute = ixn_attr
        for parent in parent_list:
            if obj_name is not None:
                parent = getattr(parent, obj_name, None)
            config_value = getattr(parent, attr, None)
            if config_value is not None:
                if translate is not None:
                    config_value = getattr(self, translate)(config_value)
                if enum_map is None:
                    attr_values.config_value = str(config_value)
                else:
                    attr_values.config_value = enum_map[str(config_value)]
            elif default_obj is None:
                attr_values.config_value = default_value
            else:
                attr_values.config_value = default_obj()
        return attr_values

    def _translate_actor_system_id(self, config_value):
        return config_value.replace(":", " ")

    def _select_protcols(self, lag_href):
        payload = {
            "selects": [
                {
                    "from": lag_href,
                    "properties": [],
                    "children": [
                        {
                            "child": "protocolStack",
                            "properties": ["*"],
                            "filters": [],
                        },
                        {
                            "child": "lagMode",
                            "properties": ["*"],
                            "filters": [],
                        },
                    ],
                    "inlines": [],
                }
            ]
        }
        url = "%s/operations/select?xpath=true" % self._api._ixnetwork.href
        results = self._api._ixnetwork._connection._execute(url, payload)
        lags = {}
        if "lag" in results[0]:
            for lag in results[0]["lag"]:
                lags[lag["name"]] = lag
        return lags

    def _set_result_value(
        self, row, column_name, column_value, column_type=str
    ):
        if (
            len(self._column_names) > 0
            and column_name not in self._column_names
        ):
            return
        try:
            row[column_name] = column_type(column_value)
        except:
            if column_type.__name__ in ["float", "int"]:
                row[column_name] = 0
            else:
                row[column_type] = column_value

    def results(self, request):
        """Return port results"""
        if request.column_names is None:
            self._column_names = []
        else:
            self._column_names = request.column_names
        port_rows = {}
        for vport in self._api.select_vports().values():
            port_row = {}
            self._set_result_value(port_row, "name", vport["name"])
            location = vport["location"]
            if vport["connectionState"].startswith("connectedLink") is True:
                location += ";connected"
            elif len(location) > 0:
                location += ";" + vport["connectionState"]
            else:
                location = vport["connectionState"]
            self._set_result_value(port_row, "location", location)
            self._set_result_value(
                port_row,
                "link",
                (
                    "up"
                    if vport["connectionState"] == "connectedLinkUp"
                    else "down"
                ),
            )
            self._set_result_value(port_row, "capture", "stopped")
            port_rows[vport["name"]] = port_row
        try:
            table = self._api.assistant.StatViewAssistant("Port Statistics")
            for row in table.Rows:
                port_row = port_rows[row["Port Name"]]
                self._set_result_value(
                    port_row, "frames_tx", row["Frames Tx."], int
                )
                self._set_result_value(
                    port_row, "frames_rx", row["Valid Frames Rx."], int
                )
                self._set_result_value(
                    port_row, "frames_tx_rate", row["Frames Tx. Rate"], float
                )
                self._set_result_value(
                    port_row,
                    "frames_rx_rate",
                    row["Valid Frames Rx. Rate"],
                    float,
                )
                self._set_result_value(
                    port_row, "bytes_tx", row["Bytes Tx."], int
                )
                self._set_result_value(
                    port_row, "bytes_rx", row["Bytes Rx."], int
                )
                self._set_result_value(
                    port_row, "bytes_tx_rate", row["Bytes Tx. Rate"], float
                )
                self._set_result_value(
                    port_row, "bytes_rx_rate", row["Bytes Rx. Rate"], float
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_0_frames_rx",
                    row["Rx Pause Priority Group 0 Frames"],
                    int,
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_1_frames_rx",
                    row["Rx Pause Priority Group 1 Frames"],
                    int,
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_2_frames_rx",
                    row["Rx Pause Priority Group 2 Frames"],
                    int,
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_3_frames_rx",
                    row["Rx Pause Priority Group 3 Frames"],
                    int,
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_4_frames_rx",
                    row["Rx Pause Priority Group 4 Frames"],
                    int,
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_5_frames_rx",
                    row["Rx Pause Priority Group 5 Frames"],
                    int,
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_6_frames_rx",
                    row["Rx Pause Priority Group 6 Frames"],
                    int,
                )
                self._set_result_value(
                    port_row,
                    "pfc_class_7_frames_rx",
                    row["Rx Pause Priority Group 7 Frames"],
                    int,
                )
        except:
            pass
        return port_rows.values()


class ProtocolAttributes(object):
    def __init__(self):
        self._ixn_attr = None
        self._config_value = []

    @property
    def ixn_attribute(self):
        return self._ixn_attr

    @ixn_attribute.setter
    def ixn_attribute(self, value):
        self._ixn_attr = value

    @property
    def config_value(self):
        return self._config_value

    @config_value.setter
    def config_value(self, value):
        self._config_value.append(value)
