"""The Subsonic response envelope — one serializer, two wire formats.

Every Subsonic endpoint answers with a ``<subsonic-response>`` wrapper carrying
``status`` / ``version`` / ``type`` / ``serverVersion``, in XML (default) or JSON
(``f=json``). All handlers return through :func:`subsonic_response`; errors go
through the rest-aware exception handler (see ``app.api.errors``).

Payload data model (shared by both formats):

* a scalar value  → an XML attribute / a JSON field
* a nested dict   → a single child element / nested object
* a list of dicts → repeated child elements / a JSON array
* the key ``"value"`` → element text content (used by e.g. lyrics)

``None`` values are dropped. Subsonic always replies with **HTTP 200**, even for
errors — the status lives inside the envelope — so clients parse the body.
"""

import json
from collections.abc import Mapping
from typing import Any
from xml.etree import ElementTree as ET

from fastapi import Response

SUBSONIC_API_VERSION = "1.16.1"
SERVER_TYPE = "mcma"
SERVER_VERSION = "0.1.0"

_XML_NS = "http://subsonic.org/restapi"
_XML_MEDIA_TYPE = "application/xml; charset=utf-8"
_JSON_MEDIA_TYPE = "application/json; charset=utf-8"


def _is_json(fmt: str | None) -> bool:
    return fmt in ("json", "jsonp")


def _scalar(value: object) -> str:
    if isinstance(value, bool):
        return "true" if value else "false"
    return str(value)


def _build_xml(parent: ET.Element, data: Mapping[str, Any]) -> None:
    for key, value in data.items():
        if value is None:
            continue
        if key == "value":
            parent.text = _scalar(value)
        elif isinstance(value, Mapping):
            _build_xml(ET.SubElement(parent, key), value)
        elif isinstance(value, list):
            for item in value:
                _build_xml(ET.SubElement(parent, key), item)
        else:
            parent.set(key, _scalar(value))


def _strip_none(value: Any) -> Any:
    """Recursively drop ``None`` values so JSON output matches XML (no empty attrs)."""
    if isinstance(value, Mapping):
        return {k: _strip_none(v) for k, v in value.items() if v is not None}
    if isinstance(value, list):
        return [_strip_none(v) for v in value]
    return value


def _render(body: Mapping[str, Any], fmt: str | None) -> Response:
    envelope: dict[str, Any] = {
        "status": body["status"],
        "version": SUBSONIC_API_VERSION,
        "type": SERVER_TYPE,
        "serverVersion": SERVER_VERSION,
        "openSubsonic": True,
        **{k: v for k, v in body.items() if k != "status"},
    }

    if _is_json(fmt):
        payload = json.dumps({"subsonic-response": _strip_none(envelope)})
        return Response(content=payload, media_type=_JSON_MEDIA_TYPE)

    root = ET.Element("subsonic-response", {"xmlns": _XML_NS})
    _build_xml(root, envelope)
    xml = b'<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(root, encoding="utf-8")
    return Response(content=xml, media_type=_XML_MEDIA_TYPE)


def subsonic_response(
    payload: Mapping[str, Any] | None = None, *, fmt: str | None = None
) -> Response:
    """A successful ``status="ok"`` envelope wrapping ``payload``."""
    body: dict[str, Any] = {"status": "ok"}
    if payload:
        body.update(payload)
    return _render(body, fmt)


def subsonic_error(code: int, message: str, *, fmt: str | None = None) -> Response:
    """A ``status="failed"`` envelope carrying a Subsonic ``<error>``."""
    body = {"status": "failed", "error": {"code": code, "message": message}}
    return _render(body, fmt)
