"""Descarga TODOS los recursos de una coleccion DAV en UNA peticion (REPORT).

Funcion impura: hace una unica peticion HTTP REPORT (`addressbook-query` para
CardDAV, `calendar-query` para CalDAV) que el servidor responde con un XML
multistatus que lleva el contenido de cada recurso INLINE (vCard / VCALENDAR).
Esto reemplaza el patron N+1 (un PROPFIND + un GET por recurso) por una sola
ida y vuelta: para 1064 contactos baja de ~9s a ~1s.

Construye el header `Authorization: Basic base64(user:pass)` a mano con stdlib y
parsea el multistatus con regex simple (sin parser XML externo), des-escapando
las entidades XML del contenido inline. 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

# Namespaces y elementos por tipo de coleccion. Xandikos (y la mayoria de
# servidores) usan el namespace "legacy" con `:ns:` que es el que aparece en el
# supported-report-set, no `urn:ietf:params:xml:carddav`.
_PROFILES = {
    "vcard": {
        "ns": "urn:ietf:params:xml:ns:carddav",
        "report": "addressbook-query",
        "data_prop": "address-data",
    },
    "ical": {
        "ns": "urn:ietf:params:xml:ns:caldav",
        "report": "calendar-query",
        "data_prop": "calendar-data",
    },
}

# Aceptamos sinonimos comunes para no atar al caller a un literal exacto.
_ALIASES = {
    "vcard": "vcard",
    "carddav": "vcard",
    "contacts": "vcard",
    "addressbook": "vcard",
    "ical": "ical",
    "icalendar": "ical",
    "caldav": "ical",
    "calendar": "ical",
}

_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,
)
_ETAG_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?getetag>\s*(.*?)\s*</(?:[A-Za-z0-9]+:)?getetag>",
    re.DOTALL | re.IGNORECASE,
)


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 _report_body(profile: dict) -> str:
    """Construye el cuerpo XML del REPORT query para el perfil dado.

    `addressbook-query` (CardDAV) no lleva filtro de tiempo: trae todos los
    vCards. `calendar-query` (CalDAV) exige un `<filter>` con un comp-filter de
    VCALENDAR; sin un comp-filter interno trae todos los componentes (todos los
    eventos), que es lo que queremos.
    """
    ns = profile["ns"]
    report = profile["report"]
    data_prop = profile["data_prop"]
    prop = "<D:prop><D:getetag/><C:%s/></D:prop>" % data_prop
    if report == "calendar-query":
        filt = '<C:filter><C:comp-filter name="VCALENDAR"/></C:filter>'
    else:
        filt = ""
    return (
        '<?xml version="1.0" encoding="utf-8" ?>'
        '<C:%s xmlns:D="DAV:" xmlns:C="%s">%s%s</C:%s>'
        % (report, ns, prop, filt, report)
    )


def _data_re(data_prop: str) -> "re.Pattern":
    """Regex para extraer el contenido inline del elemento de datos.

    El servidor namespacea el elemento (`<ns1:address-data>`); el contenido va
    XML-escapado. Capturamos el cuerpo y lo des-escapamos con html.unescape.
    """
    return re.compile(
        r"<(?:[A-Za-z0-9]+:)?%s[^>]*>(.*?)</(?:[A-Za-z0-9]+:)?%s>"
        % (re.escape(data_prop), re.escape(data_prop)),
        re.DOTALL | re.IGNORECASE,
    )


def dav_get_collection(
    base_url: str,
    username: str,
    password: str,
    collection_path: str,
    content_type: str = "vcard",
    *,
    timeout_s: float = 30.0,
    verify_tls: bool = True,
) -> dict:
    """Descarga el contenido de TODOS los recursos de una coleccion en 1 request.

    Hace un REPORT `addressbook-query` (vcard) o `calendar-query` (ical) que
    devuelve el multistatus con el contenido inline de cada recurso, evitando
    el patron N+1 (PROPFIND + un GET por recurso).

    Args:
        base_url: URL base del servidor DAV.
        username: usuario para HTTP Basic auth.
        password: contrasena para HTTP Basic auth. Resolver desde pass.
        collection_path: ruta de la coleccion (CardDAV o CalDAV).
        content_type: 'vcard' (CardDAV) o 'ical' (CalDAV). Acepta sinonimos
            ('carddav', 'contacts', 'caldav', 'calendar', ...).
        timeout_s: timeout de la peticion en segundos. Default 30.0 (la
            respuesta puede ser grande: ~600KB para 1000 contactos).
        verify_tls: si True (default) verifica el certificado TLS.

    Returns:
        dict. En exito: {status:'ok', http_status:int,
        resources:[{href:str, etag:str|None, data:str}, ...]} con un elemento
        por recurso de la coleccion (el `data` es el vCard / VCALENDAR ya
        des-escapado). En error (sin lanzar): {status:'error', error:str,
        http_status:int|None}.
    """
    key = _ALIASES.get((content_type or "").strip().lower())
    if key is None:
        return {
            "status": "error",
            "error": "content_type invalido: %r (usa 'vcard' o 'ical')"
            % content_type,
            "http_status": None,
        }
    profile = _PROFILES[key]

    url = _join_url(base_url, collection_path)
    headers = {
        "Authorization": _basic_auth_header(username, password),
        "Content-Type": "application/xml; charset=utf-8",
        # RFC 6352 / 4791: el query REPORT se aplica con Depth:1 sobre la
        # coleccion (multiget exigiria Depth:0 + lista de hrefs; query no).
        "Depth": "1",
    }
    req = urllib.request.Request(
        url,
        data=_report_body(profile).encode("utf-8"),
        method="REPORT",
        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}

    coll_tail = collection_path.strip("/").rsplit("/", 1)[-1]
    data_re = _data_re(profile["data_prop"])
    resources = []
    for block in _RESPONSE_RE.findall(xml):
        href_m = _HREF_RE.search(block)
        if not href_m:
            continue
        href = href_m.group(1).strip()
        # Skip la propia coleccion si el servidor la incluyera.
        tail = href.rstrip("/").rsplit("/", 1)[-1]
        if tail == coll_tail:
            continue
        data_m = data_re.search(block)
        if not data_m:
            # Recurso sin contenido inline (404 en ese propstat): se omite.
            continue
        data = html.unescape(data_m.group(1)).strip()
        etag_m = _ETAG_RE.search(block)
        etag = etag_m.group(1).strip() if etag_m else None
        resources.append({"href": href, "etag": etag, "data": data})
    return {"status": "ok", "http_status": status, "resources": resources}
