from abc import ABCMeta, abstractmethod
import argparse
from typing import Any, TypeAlias, TYPE_CHECKING

from .. import Client, MQTTConnectProps


if TYPE_CHECKING:
    SubParsersT: TypeAlias = argparse._SubParsersAction[argparse.ArgumentParser]  # noqa: SLF001
else:
    SubParsersT = Any


class BrokerConnectionError(Exception):
    """Failed to connect to the MQTT broker."""


def add_broker_arguments(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
    """Add common broker connection arguments to the parser."""
    parser.add_argument(
        "--address", type=str, default="localhost", help="The MQTT broker address"
    )
    parser.add_argument(
        "--client_id", type=str, default="", help="The client ID to use when connecting to the broker. By default, a random client ID is generated by the broker."
    )
    parser.add_argument(
        "--clean_start", action="store_true", help="Start a clean session",
    )
    parser.add_argument(
        "--connect_timeout", type=int, default=10, help="Connection timeout in seconds",
    )
    parser.add_argument(
        "--username", type=str, default=None, help="The username for broker authentication"
    )
    parser.add_argument(
        "--password", type=str, default=None, help="The password for broker authentication (will be encoded as UTF-8)"
    )
    parser.add_argument(
        "--session_expiry_interval", type=int, help="The session expiry interval in seconds"
    )
    parser.add_argument(
        "--receive_maximum", type=int, help="Maximum number of QoS 1 and 2 messages that can be in-flight from the broker [1-65535]"
    )
    parser.add_argument(
        "--max_packet_size", type=int, help="Maximum packet size the client is willing to accept in bytes"
    )
    parser.add_argument(
        "--topic_alias_maximum", type=int, help="Maximum topic alias the client is willing to accept [0-65535]"
    )
    parser.add_argument(
        "--request_response_info", action="store_true", help="Request response information from the broker"
    )
    parser.add_argument(
        "--request_problem_info", action="store_true", help="Request problem information from the broker"
    )
    return parser


def get_client(args: argparse.Namespace) -> Client:
    """Create and return an MQTT client connected to the specified address."""
    address = args.address
    client_id = args.client_id
    clean_start = args.clean_start
    connect_timeout = args.connect_timeout
    username = args.username
    password = args.password
    props = MQTTConnectProps()
    prop_map = {
        "session_expiry_interval": "SessionExpiryInterval",
        "receive_maximum": "ReceiveMaximum",
        "max_packet_size": "MaximumPacketSize",
        "topic_alias_maximum": "TopicAliasMaximum",
        "request_response_info": "RequestResponseInformation",
        "request_problem_info": "RequestProblemInformation",
    }
    for arg_name, prop_name in prop_map.items():
        value = getattr(args, arg_name, None)
        if value is not None:
            setattr(props, prop_name, value)
    client = Client()
    try:
        client.connect(
            address,
            client_id=client_id,
            clean_start=clean_start,
            username=username,
            password=password.encode("utf-8") if password is not None else None,
            connect_properties=props,
        )
        if not client.loop_until_connected(timeout=connect_timeout):
            raise TimeoutError("Connection to broker timed out")
        return client
    except Exception as exc:
        raise BrokerConnectionError(f"Failed to connect to MQTT broker at {address}") from exc


class Command(metaclass=ABCMeta):
    """Abstract base class for CLI commands."""

    @classmethod
    @abstractmethod
    def register(cls, sub_parsers: SubParsersT) -> None:
        """Add command-specific arguments to the parser."""

    @classmethod
    @abstractmethod
    def execute(cls, args: argparse.Namespace) -> None:
        """Execute the command with the given arguments."""
