import logging
import socket
from typing import Dict, List, Optional

from pubsub import pub
from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser, ServiceStateChange, NonUniqueNameException, \
    NotRunningException

logger = logging.getLogger()


class ZeroconfServer:
    _default_instance: Optional['ZeroconfServer'] = None

    service_type: Optional[str] = None
    server_name: Optional[str] = None
    server_port: Optional[int] = None
    properties: Dict = {}

    def __init__(self) -> None:
        self.service_type: Optional[str] = None
        self.server_name: Optional[str] = None
        self.server_port: Optional[int] = None
        self.server_ip: Optional[str] = None
        self.zeroconf: Zeroconf = Zeroconf()
        self.service_info: Optional[ServiceInfo] = None
        self.client_cache: Dict = {}
        self.properties: Dict = {}

    def _configure(self, service_type: str, server_name: str, server_port: int) -> None:
        self.service_type = service_type
        self.server_name = server_name
        self.server_port = server_port
        try:
            socket.gethostbyname(socket.gethostname())
        except socket.gaierror:
            self.stop()

    def _start(self, listen_only: bool = False) -> None:
        if not self.service_type:
            raise RuntimeError("The 'configure' method must be run before starting the zeroconf server")
        if not listen_only:
            logger.debug("Starting zeroconf service")
            self._register_service()
        else:
            logger.debug("Starting zeroconf service - Listen only mode")
        self._browse_services()

    def _stop(self) -> None:
        logger.debug("Stopping zeroconf service")
        self._unregister_service()
        if self.zeroconf:
            self.zeroconf.close()

    def _register_service(self) -> None:
        try:
            self.server_ip = socket.gethostbyname(socket.gethostname())

            info = ServiceInfo(
                self.service_type,
                f"{self.server_name}.{self.service_type}",
                addresses=[socket.inet_aton(self.server_ip)],
                port=self.server_port,
                properties=self.properties,
            )

            self.service_info = info
            self.zeroconf.register_service(info)
            logger.info(f"Registered zeroconf service: {self.service_info.name}")
        except (NonUniqueNameException, socket.gaierror) as e:
            logger.error(f"Error establishing zeroconf: {e}")

    def _unregister_service(self) -> None:
        if self.service_info:
            self.zeroconf.unregister_service(self.service_info)
            logger.info(f"Unregistered zeroconf service: {self.service_info.name}")
            self.service_info = None

    def _browse_services(self) -> None:
        ServiceBrowser(self.zeroconf, self.service_type, [self._on_service_discovered])

    def _on_service_discovered(self, zeroconf, service_type, name, state_change) -> None:
        try:
            info = zeroconf.get_service_info(service_type, name)
            hostname = name.split(f'.{self.service_type}')[0]
            logger.debug(f"Zeroconf: {hostname} {state_change}")
            if service_type == self.service_type:
                if state_change in (ServiceStateChange.Added, ServiceStateChange.Updated):
                    self.client_cache[hostname] = info
                else:
                    self.client_cache.pop(hostname, None)
                pub.sendMessage('zeroconf_state_change', hostname=hostname, state_change=state_change)
        except NotRunningException:
            pass

    @classmethod
    def _sync_class(cls) -> None:
        if cls._default_instance is not None:
            inst = cls._default_instance
            cls.service_type = inst.service_type
            cls.server_name = inst.server_name
            cls.server_port = inst.server_port
            cls.server_ip = inst.server_ip
            cls.properties = inst.properties

    def _found_hostnames(self) -> List[str]:
        local_hostname = socket.gethostname()

        def sort_key(hostname):
            return False if hostname == local_hostname else True

        sorted_hostnames = sorted(self.client_cache.keys(), key=sort_key)
        return sorted_hostnames

    def _get_hostname_properties(self, hostname: str) -> Dict:
        server_info = self.client_cache.get(hostname)
        if server_info is None:
            return {}
        decoded_server_info = {key.decode('utf-8'): value.decode('utf-8') for key, value in server_info.properties.items()}
        return decoded_server_info

    # --- Forwarders for backward compatibility ---

    @classmethod
    def configure(cls, service_type, server_name, server_port):
        if cls._default_instance is not None:
            cls._default_instance._configure(service_type, server_name, server_port)
            cls._sync_class()

    @classmethod
    def start(cls, listen_only=False):
        if cls._default_instance is not None:
            cls._default_instance._start(listen_only)

    @classmethod
    def stop(cls):
        if cls._default_instance is not None:
            cls._default_instance._stop()

    @classmethod
    def found_hostnames(cls):
        if cls._default_instance is not None:
            return cls._default_instance._found_hostnames()
        return []

    @classmethod
    def get_hostname_properties(cls, hostname):
        if cls._default_instance is not None:
            return cls._default_instance._get_hostname_properties(hostname)
        return {}


# Example usage:
if __name__ == "__main__":
    import time

    logging.basicConfig(level=logging.DEBUG)
    ZeroconfServer.configure("_zordon._tcp.local.", "foobar.local", 8080)
    try:
        ZeroconfServer.start()
        while True:
            time.sleep(0.1)
    except KeyboardInterrupt:
        pass
    finally:
        ZeroconfServer.stop()
