from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple

from inventory_monitor.logger import logger
from inventory_monitor.models import AppConfig, load_config_from_env
from inventory_monitor.snmp import SnmpCisco, SnmpCiscoLegacy, get_snmp_handler


class InventoryMonitor:
    def __init__(self, config: Optional[AppConfig] = None):
        if config is None:
            config = load_config_from_env()
        self.config = config

    def prepare_device_sn_inventory(
        self,
        items_inventory: List[Dict[str, Any]],
    ) -> Dict[str, Dict[str, Any]]:
        """Process device inventory items and organize them by serial number with prioritization.

        Items with the same serial number are prioritized based on their class name, with higher
        priority classes overriding lower priority ones.

        Args:
            items_inventory: List of inventory dictionaries from SNMP

        Returns:
            Dictionary mapping serial numbers to their inventory information
        """
        inventory_priority_classes = [
            "backplane",
            "container",
            "fan",
            "fans",
            "other",
            "powerSupply",
            "powerSupplies",
            "sensor",
            "sensors",
            "-",
            "module",
            "port",
            "chassis",
        ]

        excluded_models = self.config.model_name_excludes

        class_inventory = defaultdict(list)
        for item in items_inventory:
            class_inventory[item["entPhysicalClass"]].append(item)

        sn_inventory_dict = {}

        # First handle unrecognized classes (not in priority list)
        for class_name, items in class_inventory.items():
            if class_name not in inventory_priority_classes:
                for item in items:
                    if item.get("entPhysicalModelName") in excluded_models:
                        logger.debug(
                            f"Excluding model '{item.get('entPhysicalModelName')}' "
                            f"(serial: {item.get('entPhysicalSerialNum', '?')})"
                        )
                        continue
                    serial_num = item["entPhysicalSerialNum"]
                    sn_inventory_dict[serial_num] = item

        # Then process items according to priority (low to high)
        for priority_class in inventory_priority_classes:
            for item in class_inventory.get(priority_class, []):
                if item.get("entPhysicalModelName") in excluded_models:
                    logger.debug(
                        f"Excluding model '{item.get('entPhysicalModelName')}' "
                        f"(serial: {item.get('entPhysicalSerialNum', '?')})"
                    )
                    continue
                serial_num = item["entPhysicalSerialNum"]
                sn_inventory_dict[serial_num] = item

        return sn_inventory_dict

    def process_device(self, nb_device) -> Tuple[Any, Dict[str, Any]]:
        """Process a device to retrieve its inventory information via SNMP.

        Args:
            nb_device: NetBox device object with required attributes

        Returns:
            Tuple containing (nb_device, result_dict) where result_dict contains:
            - inventory: List of inventory items or empty list if error
            - error: Boolean indicating if an error occurred
            - error_message: String with error details or None if successful
        """
        if not nb_device.primary_ip4:
            logger.warning(
                f"{nb_device.name}: No primary IPv4 address assigned, skipping"
            )
            return (
                nb_device,
                {
                    "inventory": [],
                    "error": True,
                    "error_message": "Device does not have primary IPv4",
                },
            )

        ip_address = nb_device.primary_ip4.address.split("/")[0]

        snmp_handler = get_snmp_handler(nb_device.platform, self.config.snmp)
        logger.debug(
            f"{nb_device.name}: Using SNMP handler {type(snmp_handler).__name__} "
            f"for platform '{nb_device.platform}'"
        )

        snmp_conn = snmp_handler.get_snmp_connection(nb_device.name, ip_address)
        if not snmp_conn:
            logger.warning(
                f"{nb_device.name}: unable to establish SNMP connection...skipping..."
            )
            return (
                nb_device,
                {
                    "inventory": [],
                    "error": True,
                    "error_message": "Unable to connect via SNMP",
                },
            )

        inventory = snmp_handler.get_snmp_inventory(snmp_conn)

        # Try legacy SNMP class if no inventory with the standard class
        if not inventory and isinstance(snmp_handler, SnmpCisco):
            logger.info(
                f"{nb_device.name}: Standard Cisco Entity MIB returned empty, trying legacy OIDs"
            )
            snmp_handler = SnmpCiscoLegacy(snmp_config=self.config.snmp)
            snmp_conn = snmp_handler.get_snmp_connection(nb_device.name, ip_address)

            if not snmp_conn:
                logger.warning(
                    f"{nb_device.name}: unable to establish legacy SNMP connection...skipping..."
                )
                return (
                    nb_device,
                    {
                        "inventory": [],
                        "error": True,
                        "error_message": "Unable to connect via legacy SNMP",
                    },
                )

            inventory = snmp_handler.get_snmp_inventory(snmp_conn)

        if not inventory:
            logger.warning(f"{nb_device.name}: No inventory returned, skipping...")
            return (
                nb_device,
                {
                    "inventory": [],
                    "error": True,
                    "error_message": "Empty Inventory Parsed",
                },
            )

        human_readable_inv = snmp_handler.get_human_readable_inventory(inventory)
        handler_name = type(snmp_handler).__name__
        logger.info(
            f"{nb_device.name}: Collected {len(human_readable_inv)} inventory items "
            f"via {handler_name}"
        )

        return (
            nb_device,
            {"inventory": human_readable_inv, "error": False, "error_message": None},
        )
