"""ContentDirectory client – browse containers and items via UPnP."""

from __future__ import annotations

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

import aiohttp

from r36s_dlna_browser.dlna.models import MediaItem, ItemType, classify_upnp_class, select_best_resource

log = logging.getLogger(__name__)

_NS = {
    "s": "http://schemas.xmlsoap.org/soap/envelope/",
    "u": "urn:schemas-upnp-org:service:ContentDirectory:1",
    "didl": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
    "dc": "http://purl.org/dc/elements/1.1/",
    "upnp": "urn:schemas-upnp-org:metadata-1-0/upnp/",
}

_BROWSE_BODY = """<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
            s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
      <ObjectID>{object_id}</ObjectID>
      <BrowseFlag>BrowseDirectChildren</BrowseFlag>
      <Filter>*</Filter>
      <StartingIndex>{start}</StartingIndex>
      <RequestedCount>{count}</RequestedCount>
      <SortCriteria></SortCriteria>
    </u:Browse>
  </s:Body>
</s:Envelope>"""


def _text(el: ElementTree.Element | None) -> str:
    if el is not None and el.text:
        return el.text.strip()
    return ""


def _local_name(tag: str) -> str:
    return tag.rsplit("}", 1)[-1]


def _extract_browse_result(xml_text: str) -> str | None:
    """Extract the Browse Result payload from a SOAP response.

    DLNA servers are inconsistent here: some return a namespaced Result element,
    some return an unqualified Result element, and some may embed the DIDL as
    children instead of escaped text. This helper accepts all three shapes.
    """
    try:
        soap = ElementTree.fromstring(xml_text)
    except ElementTree.ParseError as exc:
        log.warning("SOAP parse error: %s", exc)
        return None

    for element in soap.iter():
        if _local_name(element.tag) != "Result":
            continue

        if element.text and element.text.strip():
            return element.text.strip()

        if len(element):
            return "".join(
                ElementTree.tostring(child, encoding="unicode")
                for child in element
            ).strip()

    return None


def _parse_didl(didl_xml: str, base_url: str) -> List[MediaItem]:
    """Parse a DIDL-Lite XML fragment into MediaItem objects."""
    items: list[MediaItem] = []
    try:
        root = ElementTree.fromstring(didl_xml)
    except ElementTree.ParseError as exc:
        log.warning("DIDL parse error: %s", exc)
        return items

    for container in root.findall("didl:container", _NS):
        title = _text(container.find("dc:title", _NS))
        obj_id = container.get("id", "")
        parent_id = container.get("parentID", "0")
        child_count = int(container.get("childCount", "0") or "0")
        items.append(MediaItem(
            object_id=obj_id,
            title=title,
            item_type=ItemType.CONTAINER,
            parent_id=parent_id,
            child_count=child_count,
        ))

    for item_el in root.findall("didl:item", _NS):
        title = _text(item_el.find("dc:title", _NS))
        obj_id = item_el.get("id", "")
        parent_id = item_el.get("parentID", "0")
        upnp_class = _text(item_el.find("upnp:class", _NS))
        album_art = _text(item_el.find("upnp:albumArtURI", _NS))
        item_type = classify_upnp_class(upnp_class)

        resource_url = ""
        mime_type = ""
        duration = ""
        size = 0
        resources = []
        for res_el in item_el.findall("didl:res", _NS):
            resources.append({
                "#text": res_el.text.strip() if res_el.text else "",
                "@protocolInfo": res_el.get("protocolInfo", ""),
                "@duration": res_el.get("duration", ""),
                "@size": res_el.get("size", "0"),
            })

        selected_resource = select_best_resource(resources, item_type)
        if selected_resource is not None:
            resource_url = str(selected_resource["url"])
            mime_type = str(selected_resource["mime_type"])
            duration = str(selected_resource["duration"])
            size = int(selected_resource["size"])

        # Resolve relative URLs
        if resource_url and not resource_url.startswith(("http://", "https://")):
            resource_url = base_url.rstrip("/") + "/" + resource_url.lstrip("/")
        if album_art and not album_art.startswith(("http://", "https://")):
            album_art = base_url.rstrip("/") + "/" + album_art.lstrip("/")

        items.append(MediaItem(
            object_id=obj_id,
            title=title,
            item_type=item_type,
            parent_id=parent_id,
            resource_url=resource_url,
            mime_type=mime_type,
            size=size,
            duration=duration,
            album_art_url=album_art,
        ))

    return items


async def _get_content_directory_url(
    session: aiohttp.ClientSession, location: str
) -> str | None:
    """Fetch device XML and find the ContentDirectory control URL."""
    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"}
        for svc in root.findall(".//d:service", ns):
            svc_type = _text(svc.find("d:serviceType", ns))
            if "ContentDirectory" in svc_type:
                ctrl = _text(svc.find("d:controlURL", ns))
                if ctrl:
                    # Make absolute
                    from urllib.parse import urljoin
                    return urljoin(location, ctrl)
    except Exception as exc:
        log.warning("Could not get ContentDirectory URL from %s: %s", location, exc)
    return None


class DLNAClient:
    """Browses a DLNA server's ContentDirectory."""

    def __init__(self) -> None:
        self._control_urls: dict[str, str] = {}  # location → controlURL cache

    async def browse(
        self,
        server_location: str,
        object_id: str = "0",
        start: int = 0,
        count: int = 200,
    ) -> List[MediaItem]:
        async with aiohttp.ClientSession() as session:
            ctrl_url = self._control_urls.get(server_location)
            if not ctrl_url:
                ctrl_url = await _get_content_directory_url(session, server_location)
                if not ctrl_url:
                    log.error("No ContentDirectory for %s", server_location)
                    return []
                self._control_urls[server_location] = ctrl_url

            body = _BROWSE_BODY.format(
                object_id=object_id, start=start, count=count
            )
            headers = {
                "Content-Type": 'text/xml; charset="utf-8"',
                "SOAPAction": '"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"',
            }
            try:
                async with session.post(
                    ctrl_url,
                    data=body,
                    headers=headers,
                    timeout=aiohttp.ClientTimeout(total=10),
                ) as resp:
                    xml_text = await resp.text()
            except Exception as exc:
                log.error("Browse request failed: %s", exc)
                return []

            # Extract the DIDL-Lite Result from the SOAP response
            didl_xml = _extract_browse_result(xml_text)
            if not didl_xml:
                log.warning(
                    "Empty browse result for object_id=%s via %s",
                    object_id,
                    ctrl_url,
                )
                return []

            # Derive base URL from server location for relative URL resolution
            from urllib.parse import urlparse
            parsed = urlparse(server_location)
            base_url = f"{parsed.scheme}://{parsed.netloc}"

            return _parse_didl(didl_xml, base_url)
