import os
import re

from cloudshell.devices.autoload.autoload_builder import AutoloadDetailsBuilder
from cloudshell.devices.autoload.device_names import get_device_name
from cloudshell.devices.standards.networking.autoload_structure import (
    GenericChassis,
    GenericModule,
    GenericPort,
    GenericPortChannel,
    GenericPowerPort,
    GenericResource,
    GenericSubModule,
)

from cloudshell.networking.arista.autoload.snmp_entity_table import SNMPEntityTable
from cloudshell.networking.arista.autoload.snmp_if_table import SnmpIfTable


class AristaSNMPAutoload(object):
    IF_ENTITY = "ifDescr"
    ENTITY_PHYSICAL = "entPhysicalDescr"
    SNMP_ERRORS = [r"No\s+Such\s+Object\s+currently\s+exists"]
    DEVICE_NAMES_MAP_FILE = os.path.join(
        os.path.dirname(__file__), os.pardir, "mibs", "device_names_map.csv"
    )
    PRODUCT_MIBS = ["ARISTA-PRODUCTS-MIB"]
    VENDOR = "Arista"

    def __init__(self, snmp_service, shell_name, shell_type, resource_name, logger):
        """Basic init with injected snmp handler and logger.

        :param cloudshell.snmp.quali_snmp.QualiSnmp snmp_service:
        """
        self.snmp_service = snmp_service
        self.shell_name = shell_name
        self.shell_type = shell_type
        self.resource_name = resource_name
        self.logger = logger
        self.elements = {}
        self.snmp_service.set_snmp_errors(self.SNMP_ERRORS)
        self.resource = GenericResource(
            shell_name, resource_name, resource_name, shell_type
        )

    def load_arista_mib(self):
        """Loads Arista specific mibs inside snmp handler."""
        path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "mibs"))
        self.snmp_service.update_mib_sources(path)

    def discover(self, supported_os):
        """General entry point for autoload.

        read device structure and attributes: chassis, modules, submodules, ports,
        port-channels and power supplies

        :return: AutoLoadDetails object
        :rtype: cloudshell.shell.core.driver_context.AutoLoadDetails
        """
        if not self._is_valid_device_os(supported_os):
            raise Exception(self.__class__.__name__, "Unsupported device OS")

        self.logger.info("*" * 70)
        self.logger.info("Start SNMP discovery process .....")

        self.load_arista_mib()
        self.snmp_service.load_mib(self.PRODUCT_MIBS)
        self._get_device_details()
        self._load_snmp_tables()

        if self.arista_entity.chassis_list:
            self._get_chassis_attributes(self.arista_entity.chassis_list)
            self._get_power_ports(self.arista_entity.power_port_list)
            self._get_module_attributes(self.arista_entity.module_list)
            self._get_ports_attributes(self.arista_entity.port_list)
            self._get_port_channels()
        else:
            self.logger.error("Entity table error, no chassis found")

        autoload_details = AutoloadDetailsBuilder(self.resource).autoload_details()
        self._log_autoload_details(autoload_details)
        return autoload_details

    def _is_valid_device_os(self, supported_os):
        """Validate device OS using snmp.

        :return: True or False
        """
        system_description = self.snmp_service.get_property(
            "SNMPv2-MIB", "sysDescr", "0"
        )
        self.logger.debug(
            "Detected system description: '{0}'".format(system_description)
        )
        result = re.search(
            r"({0})".format("|".join(supported_os)),
            system_description,
            flags=re.DOTALL | re.IGNORECASE,
        )

        if result:
            return True
        else:
            error_message = (
                "Incompatible driver! Please use this driver for '{0}' "
                "operation system(s)".format(str(tuple(supported_os)))
            )
            self.logger.error(error_message)
            return False

    def _load_snmp_tables(self):
        """Load all Arista required snmp tables."""
        self.logger.info("Start loading MIB tables:")
        self.if_table = SnmpIfTable(self.snmp_service, self.logger)
        self.logger.info("{0} table loaded".format(self.IF_ENTITY))
        self.arista_entity = SNMPEntityTable(
            self.snmp_service, self.logger, self.if_table
        )
        self.entity_table = self.arista_entity.get_entity_table()
        self.logger.info("Entity table loaded")

        self.logger.info("MIB Tables loaded successfully")

    def _get_chassis_attributes(self, chassis_list):
        """Get Chassis element attributes.

        :param chassis_list: list of chassis to load attributes for
        """
        self.logger.info("Building Chassis")
        for chassis in chassis_list:
            chassis_id = self.arista_entity.relative_address[chassis]

            chassis_object = GenericChassis(
                shell_name=self.shell_name,
                name="Chassis {}".format(chassis_id),
                unique_id="{}.{}.{}".format(self.resource_name, "chassis", chassis),
            )

            chassis_object.model = (
                self.snmp_service.get_property(
                    "ENTITY-MIB", "entPhysicalModelName", chassis
                )
                or self.entity_table[chassis]["entPhysicalDescr"]
            )
            chassis_object.serial_number = self.snmp_service.get_property(
                "ENTITY-MIB", "entPhysicalSerialNum", chassis
            )

            relative_address = "{0}".format(chassis_id)

            self._add_element(relative_path=relative_address, resource=chassis_object)

            self.logger.info(
                "Added " + self.entity_table[chassis]["entPhysicalDescr"] + " Chassis"
            )
        self.logger.info("Building Chassis completed")

    def _add_element(self, relative_path, resource, parent_id=""):
        """Add object data to resources and attributes lists.

        :param resource: object which contains all required data for certain resource
        """
        rel_seq = relative_path.split("/")

        if len(rel_seq) == 1:  # Chassis connected directly to root
            self.resource.add_sub_resource(relative_path, resource)
        else:
            if parent_id:
                parent_object = self.elements.get(parent_id, self.resource)
            else:
                parent_object = self.elements.get("/".join(rel_seq[:-1]), self.resource)

            rel_path = re.search(r"\d+", rel_seq[-1]).group()
            parent_object.add_sub_resource(rel_path, resource)

        self.elements.update({relative_path: resource})

    def _get_module_attributes(self, module_list):
        """Set attributes for all discovered modules."""
        self.logger.info("Building Modules")
        for module in module_list:
            module_id = self.arista_entity.relative_address.get(module)
            if not module_id:
                continue

            module_index = module_id.split("/")[-1]
            if "/" in module_id and len(module_id.split("/")) < 3:
                module_object = GenericModule(
                    shell_name=self.shell_name,
                    name="Module {}".format(module_index),
                    unique_id="{0}.{1}.{2}".format(
                        self.resource_name, "module", module
                    ),
                )

            else:
                module_object = GenericSubModule(
                    shell_name=self.shell_name,
                    name="Sub Module {}".format(module_index),
                    unique_id="{0}.{1}.{2}".format(
                        self.resource_name, "sub_module", module
                    ),
                )

            module_object.model = self.entity_table[module]["entPhysicalDescr"]
            module_object.version = self.snmp_service.get_property(
                "ENTITY-MIB", "entPhysicalSoftwareRev", module
            )
            module_object.serial_number = self.snmp_service.get_property(
                "ENTITY-MIB", "entPhysicalSerialNum", module
            )

            self._add_element(relative_path=module_id, resource=module_object)
            self.logger.info(
                "Module {} added".format(self.entity_table[module]["entPhysicalDescr"])
            )

        self.logger.info("Building Modules completed")

    def _get_power_ports(self, power_supply_list):
        """Get attributes for power ports provided in power_supply_list."""
        self.logger.info("Building PowerPorts")
        for port in power_supply_list:
            port_id = self.entity_table[port]["entPhysicalParentRelPos"]
            parent_index = int(self.entity_table[port]["entPhysicalContainedIn"])
            chassis_id = self.arista_entity.get_relative_address(parent_index)
            relative_address = "{0}/PP{1}".format(chassis_id, port_id)

            power_port = GenericPowerPort(
                shell_name=self.shell_name,
                name="PP{0}".format(power_supply_list.index(port)),
                unique_id="{0}.{1}.{2}".format(self.resource_name, "power_port", port),
            )

            power_port.model = self.snmp_service.get_property(
                "ENTITY-MIB", "entPhysicalModelName", port
            )
            power_port.port_description = self.snmp_service.get_property(
                "ENTITY-MIB", "entPhysicalDescr", port
            )
            power_port.version = self.snmp_service.get_property(
                "ENTITY-MIB", "entPhysicalHardwareRev", port
            )
            power_port.serial_number = self.snmp_service.get_property(
                "ENTITY-MIB", "entPhysicalSerialNum", port
            )

            self._add_element(
                relative_path=relative_address,
                resource=power_port,
                parent_id=chassis_id,
            )

            self.logger.info(
                "Added "
                + self.entity_table[port]["entPhysicalName"].strip(" \t\n\r")
                + " Power Port"
            )
        self.logger.info("Building Power Ports completed")

    def _get_port_channels(self):
        """Get all port channels and set attributes for them."""
        if not self.if_table:
            return
        port_channel_list = [
            if_entity
            for if_entity in self.if_table.if_entities
            if "port-channel" in if_entity.if_name.lower()
        ]
        self.logger.info("Building Port Channels")
        for if_port_channel in port_channel_list:
            interface_model = if_port_channel.if_name
            match_object = re.search(r"\d+$", interface_model)
            if match_object:
                interface_id = "{0}".format(match_object.group(0))
                associated_ports = ""
                for port in if_port_channel.associated_port_list:
                    if_port_name = self.if_table.get_if_entity_by_index(port).if_name
                    associated_ports = (
                        if_port_name.replace("/", "-").replace(" ", "") + "; "
                    )

                port_channel = GenericPortChannel(
                    shell_name=self.shell_name,
                    name=interface_model,
                    unique_id="{0}.{1}.{2}".format(
                        self.resource_name, "port_channel", interface_id
                    ),
                )

                port_channel.associated_ports = associated_ports.strip(" \t\n\r")
                port_channel.port_description = if_port_channel.if_port_description
                port_channel.ipv4_address = if_port_channel.ipv4_address
                port_channel.ipv6_address = if_port_channel.ipv6_address

                self._add_element(relative_path=interface_id, resource=port_channel)
                self.logger.info("Added " + interface_model + " Port Channel")

            else:
                self.logger.error(
                    "Adding of {0} failed. Name is invalid".format(interface_model)
                )

        self.logger.info("Building Port Channels completed")

    def _get_ports_attributes(self, port_list):
        """Get resource details and attributes for every port in port_list."""
        self.logger.info("Load Ports:")
        for port in port_list:
            port_if_entity = self.arista_entity.port_mapping.get(port)
            if not port_if_entity:
                continue
            interface_name = (
                port_if_entity.if_name or self.entity_table[port]["entPhysicalName"]
            )
            if interface_name == "":
                continue

            port_object = GenericPort(
                shell_name=self.shell_name,
                name=interface_name.replace("/", "-"),
                unique_id="{0}.{1}.{2}".format(self.resource_name, "port", port),
            )
            port_object.mac_address = port_if_entity.if_mac
            port_object.l2_protocol_type = port_if_entity.if_type
            port_object.ipv4_address = port_if_entity.ipv4_address
            port_object.ipv6_address = port_if_entity.ipv6_address
            port_object.port_description = port_if_entity.if_port_description
            port_object.bandwidth = port_if_entity.if_speed
            port_object.mtu = port_if_entity.if_mtu
            port_object.duplex = port_if_entity.duplex
            port_object.adjacent = port_if_entity.adjacent
            port_object.auto_negotiation = port_if_entity.auto_negotiation
            port_object.mac_address = port_if_entity.if_mac

            self._add_element(
                relative_path=self.arista_entity.relative_address[port],
                resource=port_object,
            )
            self.logger.info("Added " + interface_name + " Port")

        self.logger.info("Building Ports completed")

    def _get_device_details(self):
        """Get root element attributes."""
        self.logger.info("Building Root")

        self.resource.contact_name = self.snmp_service.get_property(
            "SNMPv2-MIB", "sysContact", "0"
        )
        self.resource.system_name = self.snmp_service.get_property(
            "SNMPv2-MIB", "sysName", "0"
        )
        self.resource.location = self.snmp_service.get_property(
            "SNMPv2-MIB", "sysLocation", "0"
        )
        self.resource.os_version = self._get_device_os_version()
        self.resource.model = self._get_device_model()
        self.resource.model_name = self._get_device_model_name(self.resource.model)
        self.resource.vendor = self.VENDOR

    def _get_device_os_version(self):
        """Get device OS Version from SNMPv2 mib.

        :return: version or empty string
        :rtype: str
        """
        output = self.snmp_service.get_property("SNMPv2-MIB", "sysDescr", "0")
        matched = re.search(r"version\s+(?P<version>\S+)\s+", output, re.IGNORECASE)
        if matched:
            return matched.groupdict().get("version", "")

    def _get_device_model_name(self, device_model):
        """Get device model name from the CSV file map.

        :param str device_model:  device model
        :return: device model name
        :rtype: str
        """
        return get_device_name(
            file_name=self.DEVICE_NAMES_MAP_FILE, sys_obj_id=device_model
        )

    def _get_device_model(self):
        """Get device model form snmp SNMPv2 mib.

        :return: device model
        :rtype: str
        """
        match_name = re.search(
            r"::(?P<model>\S+$)",
            self.snmp_service.get_property("SNMPv2-MIB", "sysObjectID", "0"),
        )
        return match_name.groupdict().get("model", "").capitalize()

    def _log_autoload_details(self, autoload_details):
        """Logging autoload details."""
        self.logger.debug("-------------------- <RESOURCES> ----------------------")
        for resource in autoload_details.resources:
            self.logger.debug(
                "{0:15}, {1:20}, {2}".format(
                    resource.relative_address, resource.name, resource.unique_identifier
                )
            )
        self.logger.debug("-------------------- </RESOURCES> ----------------------")

        self.logger.debug("-------------------- <ATTRIBUTES> ---------------------")
        for attribute in autoload_details.attributes:
            self.logger.debug(
                "-- {0:15}, {1:60}, {2}".format(
                    attribute.relative_address,
                    attribute.attribute_name,
                    attribute.attribute_value,
                )
            )
        self.logger.debug("-------------------- </ATTRIBUTES> ---------------------")
