"""Lista las colecciones de libreta de contactos CardDAV bajo un contacts-home (PROPFIND).

Funcion impura: hace UNA peticion HTTP PROPFIND Depth:1 sobre el directorio
contacts-home de un usuario (p.ej. `/enmanuel/contacts/`) y devuelve solo las
colecciones hijas que son libretas CardDAV de verdad — las que declaran el
resourcetype `{urn:ietf:params:xml:ns:carddav}addressbook`. Por cada una extrae
su href, su `displayname` (DAV) y, si el servidor lo expone, su descripcion
(`addressbook-description` de CardDAV). El propio contacts-home (que es una
coleccion plana sin el resourcetype addressbook) se excluye.

Es la analoga CardDAV de `dav_list_calendars`: lo que necesita un ingest o un
selector de libreta cuando el usuario tiene varias colecciones de contactos bajo
su contacts-home y hay que recorrerlas todas (o elegir una), con su nombre y su
descripcion. `dav_list_resources` solo devuelve hrefs+etag de los recursos de UNA
coleccion (las vCards), no las colecciones hijas con su metadata.

Construye el header `Authorization: Basic base64(user:pass)` a mano con stdlib y
parsea el multistatus con regex simple (sin parser XML externo), considerando
solo los `<propstat>` con estado 2xx para no recoger props que el servidor marca
404. Maneja errores sin lanzar. Solo usa stdlib (urllib, base64, re, ssl, html).
Probado contra Xandikos.
"""

import base64
import html
import re
import ssl
import urllib.error
import urllib.request

_RESPONSE_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?response>(.*?)</(?:[A-Za-z0-9]+:)?response>",
    re.DOTALL | re.IGNORECASE,
)
_HREF_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?href>\s*(.*?)\s*</(?:[A-Za-z0-9]+:)?href>",
    re.DOTALL | re.IGNORECASE,
)
_PROPSTAT_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?propstat>(.*?)</(?:[A-Za-z0-9]+:)?propstat>",
    re.DOTALL | re.IGNORECASE,
)
_STATUS_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?status>\s*(.*?)\s*</(?:[A-Za-z0-9]+:)?status>",
    re.DOTALL | re.IGNORECASE,
)
_DISPLAYNAME_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?displayname>\s*(.*?)\s*</(?:[A-Za-z0-9]+:)?displayname>",
    re.DOTALL | re.IGNORECASE,
)
# Descripcion de CardDAV: <ns:addressbook-description>texto</ns:addressbook-description>.
_DESCRIPTION_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?addressbook-description[^>]*>\s*(.*?)\s*</(?:[A-Za-z0-9]+:)?addressbook-description>",
    re.DOTALL | re.IGNORECASE,
)
# Marca de libreta CardDAV en el resourcetype. El elemento `<C:addressbook/>`
# puede venir con o sin prefijo de namespace (`<ns2:addressbook/>`, `<addressbook/>`).
_ADDRESSBOOK_TYPE_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?addressbook(?:\s[^>]*)?/?>", re.IGNORECASE
)

# El PROPFIND pide nombre, tipo y descripcion. Declarar el namespace de CardDAV
# permite que el servidor responda `addressbook-description` cuando exista.
_PROPFIND_BODY = (
    '<?xml version="1.0" encoding="utf-8" ?>'
    '<D:propfind xmlns:D="DAV:" '
    'xmlns:C="urn:ietf:params:xml:ns:carddav">'
    "<D:prop>"
    "<D:displayname/><D:resourcetype/><C:addressbook-description/>"
    "</D:prop></D:propfind>"
)


def _basic_auth_header(username: str, password: str) -> str:
    raw = ("%s:%s" % (username, password)).encode("utf-8")
    return "Basic " + base64.b64encode(raw).decode("ascii")


def _join_url(base_url: str, collection_path: str) -> str:
    return base_url.rstrip("/") + "/" + collection_path.strip("/") + "/"


def _ok_propstats(response_block: str) -> str:
    """Concatena solo los `<propstat>` con estado 2xx de un `<response>`.

    El servidor agrupa las props por estado: las presentes en un propstat 200 y
    las ausentes en un propstat 404. Tomar solo los 2xx evita leer un
    `<addressbook-description/>` vacio del bloque 404 como si fuera el valor real.
    """
    parts = []
    for ps in _PROPSTAT_RE.findall(response_block):
        status_m = _STATUS_RE.search(ps)
        if status_m and " 2" in (" " + status_m.group(1)):
            parts.append(ps)
    # Si no hay propstat (servidor minimalista), usar el bloque entero.
    return "".join(parts) if parts else response_block


def _parse_multistatus(xml_text: str, contacts_home: str) -> list:
    """Parsea un multistatus 207 y devuelve las libretas CardDAV hijas.

    Helper puro (sin red): recorre los `<response>` del XML, descarta el propio
    contacts-home y las colecciones que no declaran el resourcetype
    `carddav:addressbook`, y por cada libreta extrae href, displayname y
    addressbook-description (de los propstat 2xx). Devuelve la lista ordenada por
    nombre. Aislado para poder testear el parseo sin tocar la red.

    Args:
        xml_text: cuerpo XML del multistatus (respuesta del PROPFIND Depth:1).
        contacts_home: ruta del contacts-home (p.ej. '/enmanuel/contacts/'),
            usada para excluir la entrada del propio home.

    Returns:
        list de dicts {href:str, name:str, description:str|None}, ordenada por
        name (case-insensitive).
    """
    home_tail = contacts_home.strip("/").rsplit("/", 1)[-1]
    addressbooks = []
    for block in _RESPONSE_RE.findall(xml_text):
        href_m = _HREF_RE.search(block)
        if not href_m:
            continue
        href = href_m.group(1).strip()
        tail = href.rstrip("/").rsplit("/", 1)[-1]
        # El propio contacts-home (o un href identico al home): se excluye.
        if tail == home_tail:
            continue
        # Solo las colecciones marcadas como libreta CardDAV. El home plano no
        # lleva el resourcetype `carddav:addressbook` y queda fuera.
        if not _ADDRESSBOOK_TYPE_RE.search(block):
            continue
        ok = _ok_propstats(block)
        name_m = _DISPLAYNAME_RE.search(ok)
        name = html.unescape(name_m.group(1).strip()) if name_m else tail
        if not name:
            name = tail
        desc_m = _DESCRIPTION_RE.search(ok)
        description = html.unescape(desc_m.group(1).strip()) if desc_m else None
        if description == "":
            description = None
        addressbooks.append({"href": href, "name": name, "description": description})
    addressbooks.sort(key=lambda a: a["name"].lower())
    return addressbooks


def dav_list_addressbooks(
    base_url: str,
    username: str,
    password: str,
    contacts_home: str,
    *,
    timeout_s: float = 20.0,
    verify_tls: bool = True,
) -> dict:
    """Lista las colecciones de libreta de contactos CardDAV bajo un contacts-home.

    Hace un PROPFIND Depth:1 sobre `contacts_home` y devuelve solo las colecciones
    hijas marcadas como libreta CardDAV (resourcetype `carddav:addressbook`), con
    su nombre y descripcion. El propio `contacts_home` y cualquier coleccion
    no-addressbook se excluyen.

    Args:
        base_url: URL base del servidor DAV (p.ej. 'https://dav-x.example.com').
        username: usuario para HTTP Basic auth.
        password: contrasena para HTTP Basic auth. Resolver desde pass.
        contacts_home: ruta del contacts-home del usuario (p.ej.
            '/enmanuel/contacts/'). Las libretas de contactos cuelgan de el.
        timeout_s: timeout de la peticion en segundos. Default 20.0.
        verify_tls: si True (default) verifica el certificado TLS.

    Returns:
        dict. En exito: {status:'ok', http_status:int,
        addressbooks:[{href:str, name:str, description:str|None}, ...]} con un
        elemento por libreta de contactos (ordenadas por nombre). `description`
        es el `addressbook-description` de CardDAV si el servidor lo expone, o
        None. En error (sin lanzar): {status:'error', error:str,
        http_status:int|None}.
    """
    url = _join_url(base_url, contacts_home)
    headers = {
        "Authorization": _basic_auth_header(username, password),
        "Content-Type": "application/xml; charset=utf-8",
        "Depth": "1",
    }
    req = urllib.request.Request(
        url, data=_PROPFIND_BODY.encode("utf-8"), method="PROPFIND", headers=headers
    )

    context = None if verify_tls else ssl._create_unverified_context()
    try:
        with urllib.request.urlopen(req, timeout=timeout_s, context=context) as resp:
            status = resp.status
            xml = resp.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as e:
        return {"status": "error", "error": "http %s" % e.code, "http_status": e.code}
    except urllib.error.URLError as e:
        return {"status": "error", "error": str(e.reason), "http_status": None}
    except Exception as e:  # noqa: BLE001
        return {"status": "error", "error": str(e), "http_status": None}

    addressbooks = _parse_multistatus(xml, contacts_home)
    return {"status": "ok", "http_status": status, "addressbooks": addressbooks}
