"""mDNS peer discovery using zeroconf (optional dependency).

Falls back gracefully if zeroconf is not installed.
"""

from __future__ import annotations

import logging
import socket
from typing import Callable

log = logging.getLogger(__name__)

MDNS_SERVICE_TYPE = "_meshnet._tcp.local."

try:
    from zeroconf import ServiceInfo, ServiceBrowser, Zeroconf  # type: ignore[import]

    _HAS_ZEROCONF = True
except ImportError:
    _HAS_ZEROCONF = False


def _local_ip() -> str:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except OSError:
        return "127.0.0.1"


class MdnsDiscovery:
    """Announce this node on mDNS and discover peers on the same LAN.

    If `zeroconf` is not installed, all methods are no-ops.

    Usage::

        disc = MdnsDiscovery(
            peer_id="abc123",
            port=8001,
            on_peer_found=lambda peer_id, addr: print("found", peer_id, addr),
        )
        disc.start()
        ...
        disc.stop()
    """

    def __init__(
        self,
        peer_id: str,
        port: int,
        on_peer_found: Callable[[str, str], None] | None = None,
        on_peer_lost: Callable[[str], None] | None = None,
    ):
        self.peer_id = peer_id
        self.port = port
        self.on_peer_found = on_peer_found
        self.on_peer_lost = on_peer_lost
        self._zc: "Zeroconf | None" = None  # type: ignore[name-defined]
        self._info: "ServiceInfo | None" = None  # type: ignore[name-defined]
        self._browser = None

    def is_available(self) -> bool:
        return _HAS_ZEROCONF

    def start(self) -> None:
        if not _HAS_ZEROCONF:
            log.info("zeroconf not installed — mDNS discovery disabled")
            return
        try:
            self._zc = Zeroconf()
            local_ip = _local_ip()
            self._info = ServiceInfo(
                MDNS_SERVICE_TYPE,
                f"{self.peer_id}.{MDNS_SERVICE_TYPE}",
                addresses=[socket.inet_aton(local_ip)],
                port=self.port,
                properties={"peer_id": self.peer_id, "version": "1"},
            )
            self._zc.register_service(self._info)
            if self.on_peer_found or self.on_peer_lost:
                self._browser = ServiceBrowser(
                    self._zc, MDNS_SERVICE_TYPE, listener=_Listener(self)
                )
            log.info("mDNS announced: %s on %s:%d", self.peer_id, local_ip, self.port)
        except Exception as exc:
            log.warning("mDNS start failed: %s", exc)

    def stop(self) -> None:
        if not _HAS_ZEROCONF or self._zc is None:
            return
        try:
            if self._info:
                self._zc.unregister_service(self._info)
            self._zc.close()
        except Exception as exc:
            log.debug("mDNS stop error: %s", exc)
        self._zc = None


class _Listener:
    """Internal zeroconf service listener."""

    def __init__(self, disc: MdnsDiscovery):
        self._disc = disc

    def add_service(self, zc, type_, name):
        try:
            info = zc.get_service_info(type_, name)
            if info is None:
                return
            remote_peer_id = (info.properties or {}).get(b"peer_id", b"").decode()
            if remote_peer_id == self._disc.peer_id:
                return  # ignore self
            addr = f"http://{socket.inet_ntoa(info.addresses[0])}:{info.port}"
            if self._disc.on_peer_found:
                self._disc.on_peer_found(remote_peer_id, addr)
        except Exception as exc:
            log.debug("mDNS add_service error: %s", exc)

    def remove_service(self, zc, type_, name):
        try:
            # name is like "peer_id._meshnet._tcp.local."
            peer_id = name.split(".")[0]
            if self._disc.on_peer_lost:
                self._disc.on_peer_lost(peer_id)
        except Exception as exc:
            log.debug("mDNS remove_service error: %s", exc)

    def update_service(self, zc, type_, name):
        pass
