from Model.MQTTSubject import MQTTSubject
from paho.mqtt.client import Client, MQTTMessage
from typing import Any, List
import logging

class MQTTClient(MQTTSubject):
    _instance = None 

    def __new__(cls, *args, **kwargs):
        """
        Singleton pattern implementation.
        """
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    

    def __init__(self, *args):
        """
        Initializes the MQTTClient instance.
        This method is called only once due to the singleton pattern.
        It initializes the MQTT client and sets up the necessary attributes.
        """
        if hasattr(self, "_initialized"): # If the client is already initialized
            self._initialized = True
            return 

        super().__init__()
        self._client = Client()
        self._subscribe_topics: List[str] = []
        self._initialized = True # Marks the client as initialized
    

    @classmethod
    def delete_instance(cls):
        """
        Deletes the singleton instance and stops the MQTT client loop safely.
        This allows a fresh instance to be created afterwards.
        """
        if cls._instance is not None:
            instance = cls._instance
            instance._client = None
            instance._subscribe_topics = []
            instance._initialized = False
            cls._instance = None
            logging.info("MQTTClient instance deleted.")
    
    def connect(self, broker_ip: str, broker_port: int, broker_username: str = None, broker_password:str = None, tls_cert: str = None) -> None:
        """
        Connects the client to the broker.
        
        Args:
        broker_ip (str): The IP address of the MQTT broker.
        broker_port (int): The port of the MQTT broker.
        broker_username (str): The username to connect to the broker.
        broker_password (str): The password to connect to the broker.
        tls_cert (str): The TLS certificate path to use for the connection.
        
        Raises:
        ConnectionRefusedError: If the connection to the broker fails.
        """
        self._broker_ip = broker_ip
        self._broker_port = broker_port
        self._broker_username = broker_username
        self._broker_password = broker_password

        if not self._client.is_connected():
            if self._broker_username and self._broker_password: # If username and password are provided
                self._client.username_pw_set(self._broker_username, self._broker_password)
            
            if tls_cert: # If a TLS certificate is provided
                self._client.tls_set(tls_cert)

            try:
                self._client.on_message = self._on_message
                self._client.connect(self._broker_ip, self._broker_port)
                self._client.loop_start()
            except ConnectionRefusedError as e:
                logging.error(f"Failed to connect to MQTT broker at {self._broker_ip}:{self._broker_port} with error: {e}")
                raise ConnectionRefusedError(self._broker_ip, self._broker_port, -1)
        else:
            logging.warning(f"Attempting to connect to MQTT broker at {self._broker_ip}:{self._broker_port} when client is already connected")


    def disconnect(self) -> None:
        """
        Disconnects the client from the broker.
        """
        if self._client.is_connected():
            self._client.disconnect()
            logging.info(f"Disconnecting from MQTT broker at {self._broker_ip}:{self._broker_port}")
        else:
            logging.warning(f"Attempting to disconnect from MQTT broker at {self._broker_ip}:{self._broker_port} when client is not connected")
            
    def loop_stop(self) -> None:
        """
        Stops the MQTT client loop.
        """
        self._client.loop_stop()
        logging.info(f"Stopping MQTT client loop")
    
    def set_on_connect(self, on_connect: Any) -> None:
        """
        Sets the error handler for the client.
        
        Args:
        error_handler (Any): The error handler to set.
        """
        self._client.on_connect = on_connect


    def subscribe(self, topic: str) -> None:
        """
        Adds a topic to the list of topics to subscribe to.

        Args:
        topic (Topic): The topic to subscribe to.

        Raises:

        TypeError: If the topic is not an instance of the Topic class.
        """
        if topic not in self._subscribe_topics:
            self._subscribe_topics.append(topic)
        self._client.subscribe(topic)
        logging.info(f"Subscribed to topic {topic}")


    def _on_message(self, client: Client, userdata: Any, message: MQTTMessage) -> None:
        """
        Callback function that is called when a message is received on a subscribed topic.
        
        Args:
        client (Client): The client that received the message.
        userdata (Any): User data that is passed to the callback.
        message (MQTTMessage): The message that was received.
        """
        self.notify_observers(client, message, message.topic)



    def publish(self, topic: str, payload: str) -> None:
        """
        Publishes a message to a topic.
        
        Args:
        topic (Topic): The topic to publish the message to.
        payload (str): The message to publish.
        
        Raises:
        RuntimeError: If the client is not connected.
        """
        if self._client.is_connected():
            self._client.publish(str(topic), payload)
            logging.info(f"Published message to topic {topic}")
        else:
            logging.error(f"Failed to publish message to topic {topic} because the client is not connected")
        