"""Peer discovery using Zeroconf/mDNS."""

from __future__ import annotations

import logging
import socket
from typing import Any, Callable

logger = logging.getLogger(__name__)

SERVICE_TYPE = "_neurovault._tcp.local."


class PeerDiscovery:
    """Discover NEUROVAULT peers on local network via mDNS."""

    def __init__(
        self,
        device_id: str,
        port: int = 9473,
        on_peer_found: Callable[[dict[str, Any]], None] | None = None,
        on_peer_lost: Callable[[dict[str, Any]], None] | None = None,
    ):
        self._device_id = device_id
        self._port = int(port)
        self._on_found = on_peer_found
        self._on_lost = on_peer_lost
        self._peers: dict[str, dict[str, Any]] = {}
        self._zeroconf = None
        self._browser = None
        self._info = None

    def start(self) -> None:
        """Start mDNS advertisement and peer browsing."""
        try:
            from zeroconf import ServiceBrowser, ServiceInfo, Zeroconf
        except Exception:  # pragma: no cover - optional dependency
            logger.warning("zeroconf not installed, mDNS discovery disabled")
            return

        self._zeroconf = Zeroconf()
        self._info = ServiceInfo(
            SERVICE_TYPE,
            f"neurovault-{self._device_id}.{SERVICE_TYPE}",
            addresses=[socket.inet_aton(self._get_local_ip())],
            port=self._port,
            properties={
                "device_id": self._device_id,
                "version": "0.3.1",
            },
        )
        self._zeroconf.register_service(self._info)
        self._browser = ServiceBrowser(self._zeroconf, SERVICE_TYPE, handlers=[self._on_service_change])
        logger.info("mDNS discovery started on port %d", self._port)

    def stop(self) -> None:
        if self._zeroconf is None:
            return
        try:
            if self._info is not None:
                self._zeroconf.unregister_service(self._info)
        except Exception as exc:
            logger.debug("Failed to unregister service info: %s", exc)
        try:
            self._zeroconf.unregister_all_services()
        except Exception as exc:
            logger.debug("Failed to unregister all services: %s", exc)
        self._zeroconf.close()
        self._zeroconf = None

    def get_peers(self) -> dict[str, dict[str, Any]]:
        return dict(self._peers)

    def _on_service_change(self, zeroconf, service_type: str, name: str, state_change) -> None:
        try:
            from zeroconf import ServiceStateChange
        except Exception:
            return

        if state_change == ServiceStateChange.Added:
            info = zeroconf.get_service_info(service_type, name)
            if not info or not info.properties:
                return

            device_id = info.properties.get(b"device_id", b"").decode()
            if not device_id or device_id == self._device_id:
                return

            addresses = info.parsed_addresses()
            peer = {
                "device_id": device_id,
                "host": addresses[0] if addresses else "",
                "port": int(info.port),
                "version": info.properties.get(b"version", b"0.3.1").decode(),
            }
            self._peers[device_id] = peer
            if self._on_found:
                self._on_found(peer)
            logger.info("Discovered peer: %s at %s:%d", device_id, peer["host"], peer["port"])
            return

        if state_change == ServiceStateChange.Removed:
            for did, peer in list(self._peers.items()):
                if name.startswith(f"neurovault-{did}"):
                    self._peers.pop(did, None)
                    if self._on_lost:
                        self._on_lost(peer)

    @staticmethod
    def _get_local_ip() -> str:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            sock.connect(("8.8.8.8", 80))
            return sock.getsockname()[0]
        except Exception as exc:
            logger.debug("Unable to detect local IP, defaulting to loopback: %s", exc)
            return "127.0.0.1"
        finally:
            sock.close()
