"""Abstract base class for SNMP handlers."""

from abc import ABC, abstractmethod
from typing import Dict, List, Optional

from easysnmp import Session
from easysnmp.exceptions import EasySNMPError

from inventory_monitor.logger import logger
from inventory_monitor.models import SnmpConfig, load_config_from_env

from .constants import OID
from .errors import sanitize_snmp_error

# Minimum length for valid serial numbers
SERIAL_MIN_LENGTH = 4


class ISnmp(ABC):
    """Abstract base class defining the interface for SNMP handlers."""

    def __init__(self, snmp_config: Optional[SnmpConfig] = None):
        if snmp_config is None:
            snmp_config = load_config_from_env().snmp
        self.snmp_config = snmp_config

    @abstractmethod
    def get_snmp_connection(
        self, device_name: str, device_ip: str
    ) -> Optional[Session]:
        """Establish an SNMP connection to the device.

        Args:
            device_name: Name of the device
            device_ip: IP address of the device

        Returns:
            SNMP session or None if connection failed
        """

    @abstractmethod
    def get_snmp_inventory(
        self, snmp_connection: Session
    ) -> Optional[Dict[str, Dict[str, str]]]:
        """Retrieve inventory data from device via SNMP.

        Args:
            snmp_connection: Established SNMP session

        Returns:
            Dictionary of inventory data or None if retrieval failed
        """

    @abstractmethod
    def get_human_readable_inventory(
        self, inventory: Dict[str, Dict[str, str]]
    ) -> List[Dict[str, str]]:
        """Convert raw SNMP inventory data to human-readable format.

        Args:
            inventory: Raw inventory data

        Returns:
            List of inventory items in standardized format
        """

    @staticmethod
    def _add_to_inventory(
        inventory: Dict[str, Dict[str, str]], key: str, field: str, value: str
    ) -> None:
        """Add a value to the inventory dict, creating the sub-dict if needed."""
        inventory.setdefault(key, {})[field] = value

    @staticmethod
    def is_valid_serial(serial_num: str, min_length: int = SERIAL_MIN_LENGTH) -> bool:
        """Check if a serial number is valid according to standard criteria.

        Args:
            serial_num: Serial number to validate
            min_length: Minimum length for a valid serial number

        Returns:
            True if serial number is valid, False otherwise
        """
        return (
            serial_num
            and isinstance(serial_num, str)
            and len(serial_num.strip()) >= min_length
            and serial_num.strip().upper() != "BUILTIN"
        )

    def snmp_session(
        self, device_name: str, device_ip: str, bulkwalk_oid: str
    ) -> Optional[Session]:
        """Create an SNMP session trying different SNMP versions.

        Uses self.snmp_config for credentials, timeout, and retries.

        Args:
            device_name: Name of the device
            device_ip: IP address of the device
            bulkwalk_oid: OID to test connectivity

        Returns:
            SNMP session or None if all connection attempts fail
        """
        cfg = self.snmp_config

        # Try SNMP v3 connection
        if 3 in cfg.versions:
            for attempt in range(1, cfg.retries + 1):
                try:
                    security_level = cfg.v3.security_level or ""
                    security_username = cfg.v3.user or ""
                    auth_protocol = cfg.v3.auth_protocol or ""
                    auth_password = cfg.v3.auth_password or ""
                    privacy_protocol = cfg.v3.privacy_protocol or ""
                    privacy_password = cfg.v3.privacy_password or ""

                    session_params = {
                        "hostname": device_ip,
                        "version": 3,
                        "security_level": security_level,
                        "security_username": security_username,
                        "use_numeric": True,
                        "timeout": cfg.timeout,
                    }

                    if auth_protocol and auth_protocol.strip():
                        session_params["auth_protocol"] = auth_protocol.strip()
                        session_params["auth_password"] = auth_password

                    if privacy_protocol and privacy_protocol.strip():
                        session_params["privacy_protocol"] = privacy_protocol.strip()
                        session_params["privacy_password"] = privacy_password

                    snmp_connection = Session(**session_params)

                    try:
                        snmp_connection.get(OID["SYS_DESCR"])
                        snmp_connection.bulkwalk(bulkwalk_oid)
                        logger.success(
                            f"{device_name} ({device_ip}): SNMPv3 connection successful"
                        )
                        return snmp_connection
                    except (SystemError, EasySNMPError) as inner_error:
                        raise EasySNMPError(f"Bulkwalk failed: {sanitize_snmp_error(inner_error)}")

                except (SystemError, EasySNMPError) as e:
                    error_msg = str(e).lower()
                    if attempt < cfg.retries:
                        logger.debug(
                            f"{device_name} ({device_ip}): SNMPv3 attempt {attempt}/{cfg.retries} failed, retrying"
                        )
                        continue

                    if "authentication failure" in error_msg:
                        logger.warning(
                            f"{device_name} ({device_ip}): SNMPv3 authentication failed - check username and password/keys"
                        )
                    elif "timeout" in error_msg:
                        logger.warning(
                            f"{device_name} ({device_ip}): SNMPv3 timeout - device may not be reachable or SNMPv3 not configured"
                        )
                    elif "unknown user" in error_msg:
                        logger.warning(
                            f"{device_name} ({device_ip}): SNMPv3 unknown user '{security_username}'"
                        )
                    else:
                        logger.warning(
                            f"{device_name} ({device_ip}): Unable to use SNMPv3 - {sanitize_snmp_error(e)}"
                        )

        # Try SNMP v2 connection
        if 2 in cfg.versions:
            if cfg.v2.community == "public":
                logger.warning(
                    f"{device_name}: Using default SNMPv2 community 'public' — "
                    "consider setting SNMP_COMMUNITY to a non-default value"
                )
            for attempt in range(1, cfg.retries + 1):
                try:
                    snmp_connection = Session(
                        hostname=device_ip,
                        version=2,
                        community=cfg.v2.community or "",
                        use_numeric=True,
                        timeout=cfg.timeout,
                    )
                    snmp_connection.bulkwalk(bulkwalk_oid)
                    logger.success(
                        f"{device_name} ({device_ip}): SNMPv2 connection successful"
                    )
                    return snmp_connection
                except (SystemError, EasySNMPError) as e:
                    if attempt < cfg.retries:
                        logger.debug(
                            f"{device_name} ({device_ip}): SNMPv2 attempt {attempt}/{cfg.retries} failed, retrying"
                        )
                        continue
                    logger.warning(
                        f"{device_name} ({device_ip}): Unable to use SNMPv2 - {sanitize_snmp_error(e)}"
                    )

        logger.error(
            f"{device_name} ({device_ip}): All SNMP connection attempts failed"
        )
        return None
