"""Zero-config LAN discovery for Chatixia agents and hubs using UDP multicast."""

from __future__ import annotations

import json
import socket
import struct
import threading
import time
from typing import Any, Callable

# Multicast group and port for Chatixia discovery
MULTICAST_GROUP = "239.77.67.88"  # arbitrary link-local multicast
MULTICAST_PORT = 8888

# Message types
MSG_AGENT_ANNOUNCE = "agent"
MSG_HUB_ANNOUNCE = "hub"


def _get_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 Exception:
        return "127.0.0.1"


class Announcer:
    """Periodically broadcasts a JSON announcement via UDP multicast."""

    def __init__(
        self,
        msg_type: str,
        payload: dict[str, Any],
        interval: int = 10,
    ) -> None:
        self._msg_type = msg_type
        self._payload = payload
        self._interval = interval
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None

    def _loop(self) -> None:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
        try:
            while not self._stop_event.is_set():
                msg = json.dumps(
                    {
                        "type": self._msg_type,
                        **self._payload,
                        "ip": _get_local_ip(),
                        "hostname": socket.gethostname(),
                        "timestamp": time.time(),
                    }
                ).encode()
                try:
                    sock.sendto(msg, (MULTICAST_GROUP, MULTICAST_PORT))
                except Exception:
                    pass
                self._stop_event.wait(self._interval)
        finally:
            sock.close()

    def start(self) -> None:
        if self._thread is not None:
            return
        self._thread = threading.Thread(target=self._loop, daemon=True, name="announcer")
        self._thread.start()

    def stop(self) -> None:
        self._stop_event.set()
        if self._thread is not None:
            self._thread.join(timeout=2)
            self._thread = None


class Listener:
    """Listens for multicast announcements on the LAN."""

    def __init__(self, on_message: Callable[[dict[str, Any]], None]) -> None:
        self._on_message = on_message
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None

    def _loop(self) -> None:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        except AttributeError:
            pass  # SO_REUSEPORT not available on all platforms
        sock.bind(("", MULTICAST_PORT))

        # Join multicast group
        mreq = struct.pack("4sl", socket.inet_aton(MULTICAST_GROUP), socket.INADDR_ANY)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
        sock.settimeout(1.0)

        try:
            while not self._stop_event.is_set():
                try:
                    data, _ = sock.recvfrom(4096)
                    msg = json.loads(data.decode())
                    self._on_message(msg)
                except socket.timeout:
                    continue
                except Exception:
                    continue
        finally:
            sock.close()

    def start(self) -> None:
        if self._thread is not None:
            return
        self._thread = threading.Thread(target=self._loop, daemon=True, name="discovery-listener")
        self._thread.start()

    def stop(self) -> None:
        self._stop_event.set()
        if self._thread is not None:
            self._thread.join(timeout=2)
            self._thread = None
