"""SSDP-based discovery of DLNA MediaServers on the local network."""

from __future__ import annotations

import asyncio
import logging
import socket
from typing import List
from xml.etree import ElementTree

import aiohttp

from r36s_dlna_browser.dlna.models import MediaServer

log = logging.getLogger(__name__)

SSDP_ADDR = "239.255.255.250"
SSDP_PORT = 1900
MEDIA_SERVER_TYPE = "urn:schemas-upnp-org:device:MediaServer:1"
SSDP_MX = 3
SEARCH_TIMEOUT = 5.0

_SSDP_SEARCH = (
    "M-SEARCH * HTTP/1.1\r\n"
    f"HOST: {SSDP_ADDR}:{SSDP_PORT}\r\n"
    'MAN: "ssdp:discover"\r\n'
    f"MX: {SSDP_MX}\r\n"
    f"ST: {MEDIA_SERVER_TYPE}\r\n"
    "\r\n"
).encode()


class _SSDPProtocol(asyncio.DatagramProtocol):
    """Collects M-SEARCH responses."""

    def __init__(self) -> None:
        self.responses: list[tuple[str, str]] = []  # (location, server)

    def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
        try:
            text = data.decode("utf-8", errors="replace")
        except Exception:
            return
        headers: dict[str, str] = {}
        for line in text.split("\r\n"):
            if ":" in line:
                key, _, val = line.partition(":")
                headers[key.strip().upper()] = val.strip()
        location = headers.get("LOCATION", "")
        st = headers.get("ST", "")
        if location and MEDIA_SERVER_TYPE in st:
            self.responses.append((location, headers.get("SERVER", "")))

    def error_received(self, exc: Exception) -> None:
        log.debug("SSDP error: %s", exc)

    def connection_lost(self, exc: Exception | None) -> None:
        pass


async def _fetch_friendly_name(session: aiohttp.ClientSession, location: str) -> str:
    """Fetch the device description XML and extract friendlyName."""
    try:
        async with session.get(location, timeout=aiohttp.ClientTimeout(total=5)) as resp:
            xml_text = await resp.text()
        root = ElementTree.fromstring(xml_text)
        ns = {"d": "urn:schemas-upnp-org:device-1-0"}
        el = root.find(".//d:device/d:friendlyName", ns)
        if el is not None and el.text:
            return el.text.strip()
    except Exception as exc:
        log.debug("Could not fetch description from %s: %s", location, exc)
    return location


class DLNADiscovery:
    """Find DLNA MediaServers via SSDP M-SEARCH."""

    async def find_servers(self, timeout: float = SEARCH_TIMEOUT) -> List[MediaServer]:
        loop = asyncio.get_running_loop()
        transport, protocol = await loop.create_datagram_endpoint(
            _SSDPProtocol,
            local_addr=("0.0.0.0", 0),
            family=socket.AF_INET,
        )
        assert isinstance(protocol, _SSDPProtocol)
        try:
            transport.sendto(_SSDP_SEARCH, (SSDP_ADDR, SSDP_PORT))
            await asyncio.sleep(timeout)
        finally:
            transport.close()

        seen: dict[str, str] = {}
        for loc, srv in protocol.responses:
            if loc not in seen:
                seen[loc] = srv

        servers: list[MediaServer] = []
        if seen:
            async with aiohttp.ClientSession() as session:
                for loc in seen:
                    name = await _fetch_friendly_name(session, loc)
                    servers.append(MediaServer(friendly_name=name, location=loc))
        log.info("Discovered %d DLNA server(s).", len(servers))
        return servers
