"""Opt-in mDNS advertisement. Discovery is metadata, never authorization."""

from __future__ import annotations

from dataclasses import dataclass
import ipaddress
import socket
import threading
from typing import Mapping


SERVICE_TYPE = "_omni-node._tcp.local."


@dataclass(frozen=True, slots=True)
class DiscoveryStatus:
    enabled: bool
    registered: bool
    service_type: str = SERVICE_TYPE
    error: str | None = None

    def as_dict(self) -> dict[str, object]:
        return {
            "enabled": self.enabled,
            "registered": self.registered,
            "service_type": self.service_type,
            "error": self.error,
            "trust": "discovery-never-implies-trust",
        }


def minimal_txt(*, node_id: str, port: int, pairing_state: str) -> dict[str, str]:
    return {
        "protocol_version": "1.0",
        "node_id": node_id,
        "pairing_state": pairing_state,
        "api_port": str(port),
    }


class MdnsAdvertiser:
    def __init__(self, *, enabled: bool, host: str, port: int, node_id: str) -> None:
        self.enabled = enabled
        self.host = host
        self.port = port
        self.node_id = node_id
        self._lock = threading.RLock()
        self._zeroconf = None
        self._info = None
        self._status = DiscoveryStatus(enabled, False)

    @property
    def status(self) -> DiscoveryStatus:
        return self._status

    def start(self, *, pairing_state: str) -> bool:
        if not self.enabled:
            return False
        with self._lock:
            if self._status.registered:
                return True
            try:
                from zeroconf import ServiceInfo, Zeroconf

                address = ipaddress.ip_address(self.host)
                hostname = "".join(
                    character if character.isalnum() or character == "-" else "-"
                    for character in socket.gethostname()
                ).strip("-") or "omni-node"
                properties: Mapping[str, str] = minimal_txt(
                    node_id=self.node_id,
                    port=self.port,
                    pairing_state=pairing_state,
                )
                info = ServiceInfo(
                    SERVICE_TYPE,
                    f"{self.node_id}.{SERVICE_TYPE}",
                    addresses=[address.packed],
                    port=self.port,
                    properties=properties,
                    server=f"{hostname}.local.",
                )
                zeroconf = Zeroconf()
                zeroconf.register_service(info, allow_name_change=False)
                self._zeroconf = zeroconf
                self._info = info
                self._status = DiscoveryStatus(True, True)
                return True
            except Exception as exc:
                self._status = DiscoveryStatus(
                    True,
                    False,
                    error=f"mDNS unavailable: {type(exc).__name__}",
                )
                return False

    def update_pairing_state(self, pairing_state: str) -> None:
        with self._lock:
            zeroconf, previous = self._zeroconf, self._info
            if zeroconf is None or previous is None:
                return
            try:
                from zeroconf import ServiceInfo

                info = ServiceInfo(
                    previous.type,
                    previous.name,
                    addresses=list(previous.addresses),
                    port=previous.port,
                    properties=minimal_txt(
                        node_id=self.node_id,
                        port=self.port,
                        pairing_state=pairing_state,
                    ),
                    server=previous.server,
                )
                zeroconf.update_service(info)
                self._info = info
            except Exception as exc:
                self._status = DiscoveryStatus(
                    True,
                    self._status.registered,
                    error=f"mDNS update unavailable: {type(exc).__name__}",
                )

    def stop(self) -> None:
        with self._lock:
            zeroconf, info = self._zeroconf, self._info
            self._zeroconf = None
            self._info = None
            self._status = DiscoveryStatus(self.enabled, False, error=self._status.error)
        if zeroconf is not None:
            try:
                if info is not None:
                    zeroconf.unregister_service(info)
            except Exception as exc:
                self._status = DiscoveryStatus(
                    self.enabled,
                    False,
                    error=f"mDNS unregister unavailable: {type(exc).__name__}",
                )
            try:
                zeroconf.close()
            except Exception as exc:
                self._status = DiscoveryStatus(
                    self.enabled,
                    False,
                    error=f"mDNS close unavailable: {type(exc).__name__}",
                )
