"""Lista las colecciones de calendario CalDAV bajo un calendar-home (PROPFIND).

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

Esto es lo que necesita un selector de calendario en una UI: el usuario tiene
varias colecciones bajo su calendar-home y quiere elegir una, con su nombre y su
color. `dav_list_resources` solo devuelve hrefs+etag de los recursos de UNA
coleccion (los eventos), 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,
)
# Color de Apple: <ns:calendar-color>#RRGGBBAA</ns:calendar-color> (o sin alfa).
_COLOR_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?calendar-color[^>]*>\s*(.*?)\s*</(?:[A-Za-z0-9]+:)?calendar-color>",
    re.DOTALL | re.IGNORECASE,
)
# Marca de calendario CalDAV en el resourcetype. El elemento `<C:calendar/>`
# puede venir con o sin prefijo de namespace (`<ns2:calendar/>`, `<calendar/>`).
_CALENDAR_TYPE_RE = re.compile(
    r"<(?:[A-Za-z0-9]+:)?calendar(?:\s[^>]*)?/?>", re.IGNORECASE
)

# El PROPFIND pide nombre, tipo y color (Apple). Declarar el namespace de Apple y
# el de CalDAV permite que el servidor responda esas props cuando existan.
_PROPFIND_BODY = (
    '<?xml version="1.0" encoding="utf-8" ?>'
    '<D:propfind xmlns:D="DAV:" '
    'xmlns:A="http://apple.com/ns/ical/" '
    'xmlns:C="urn:ietf:params:xml:ns:caldav">'
    "<D:prop>"
    "<D:displayname/><D:resourcetype/><A:calendar-color/>"
    "</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
    `<calendar-color/>` 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 dav_list_calendars(
    base_url: str,
    username: str,
    password: str,
    home_path: str,
    *,
    timeout_s: float = 15.0,
    verify_tls: bool = True,
) -> dict:
    """Lista las colecciones de calendario CalDAV bajo un calendar-home.

    Hace un PROPFIND Depth:1 sobre `home_path` y devuelve solo las colecciones
    hijas marcadas como calendario CalDAV (resourcetype `caldav:calendar`), con
    su nombre y color. El propio `home_path` y cualquier coleccion no-calendario
    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.
        home_path: ruta del calendar-home del usuario (p.ej.
            '/enmanuel/calendars/'). Las colecciones de calendario cuelgan de el.
        timeout_s: timeout de la peticion en segundos. Default 15.0.
        verify_tls: si True (default) verifica el certificado TLS.

    Returns:
        dict. En exito: {status:'ok', http_status:int,
        calendars:[{href:str, name:str, color:str|None}, ...]} con un elemento
        por coleccion de calendario (ordenadas por nombre). `color` es el
        `calendar-color` de Apple (ej. '#FF2968FF') si el servidor lo expone, o
        None. En error (sin lanzar): {status:'error', error:str,
        http_status:int|None}.
    """
    url = _join_url(base_url, home_path)
    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}

    home_tail = home_path.strip("/").rsplit("/", 1)[-1]
    calendars = []
    for block in _RESPONSE_RE.findall(xml):
        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 calendar-home (o un href identico al home): se excluye.
        if tail == home_tail:
            continue
        # Solo las colecciones marcadas como calendario CalDAV. El home plano no
        # lleva el resourcetype `caldav:calendar` y queda fuera.
        if not _CALENDAR_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
        color_m = _COLOR_RE.search(ok)
        color = html.unescape(color_m.group(1).strip()) if color_m else None
        if color == "":
            color = None
        calendars.append({"href": href, "name": name, "color": color})
    calendars.sort(key=lambda c: c["name"].lower())
    return {"status": "ok", "http_status": status, "calendars": calendars}
