"""Crea una nueva coleccion de calendario CalDAV bajo un calendar-home.

Funcion impura: hace una peticion HTTP MKCALENDAR (metodo HTTP literal) para
crear una "agenda" nueva bajo el calendar-home de un principal, y opcionalmente
un PROPPATCH posterior para fijarle el color (Apple `calendar-color`) y la
descripcion (`{urn:ietf:params:xml:ns:caldav}calendar-description`). El nombre
visible (DAV:displayname) se setea ya en el cuerpo del MKCALENDAR.

El slug (segmento de path de la coleccion) se sanea a `[a-z0-9_-]` (minusculas,
espacios -> '-'); si queda vacio se devuelve un error de validacion. La
coleccion se crea en `<calendar_home><slug>/`.

Idempotente: un 201 (Created) es exito; un 405 (Method Not Allowed) o un 301
(la coleccion ya existe en ese path) se devuelven como
{status:'ok', existed:True}. El display_name y la description se escapan para
XML. Construye `Authorization: Basic base64(user:pass)` a mano con stdlib.
Maneja errores sin lanzar (salvo validacion de args). Solo usa stdlib
(urllib, base64, re, ssl, xml.sax.saxutils). Probado contra Xandikos.
"""

import base64
import re
import ssl
import urllib.error
import urllib.request
from xml.sax.saxutils import escape as _xml_escape

_UNSAFE_SLUG_RE = re.compile(r"[^a-z0-9_-]")


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 _sanitize_slug(slug: str) -> str:
    """Sanea un slug a `[a-z0-9_-]`.

    Pasa a minusculas, convierte espacios (y runs de espacios) en un guion, y
    elimina cualquier otro caracter no permitido. Colapsa guiones repetidos y
    recorta guiones de los extremos. Puede devolver "" si no queda nada usable;
    el caller trata "" como error de validacion.
    """
    s = slug.strip().lower()
    s = re.sub(r"\s+", "-", s)
    s = _UNSAFE_SLUG_RE.sub("", s)
    s = re.sub(r"-{2,}", "-", s).strip("-")
    return s


def _build_mkcalendar_xml(display_name: str) -> str:
    """Cuerpo XML minimo del MKCALENDAR que setea el displayname (escapado)."""
    name = _xml_escape(display_name)
    return (
        '<?xml version="1.0" encoding="utf-8" ?>'
        '<C:mkcalendar xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:D="DAV:">'
        "<D:set><D:prop>"
        "<D:displayname>%s</D:displayname>"
        "</D:prop></D:set>"
        "</C:mkcalendar>"
    ) % name


def _build_proppatch_xml(color: str = "", description: str = "") -> str:
    """Cuerpo XML del PROPPATCH que fija color (Apple) y/o descripcion (CalDAV).

    Solo incluye las props no vacias. El color va como `calendar-color` del
    namespace `http://apple.com/ns/ical/` con el hex tal cual lo pasa el caller
    (p.ej. '#RRGGBB'). La descripcion es `calendar-description` de CalDAV. Ambos
    valores se escapan para XML.
    """
    props = []
    if color:
        props.append("<A:calendar-color>%s</A:calendar-color>" % _xml_escape(color))
    if description:
        props.append(
            "<C:calendar-description>%s</C:calendar-description>"
            % _xml_escape(description)
        )
    return (
        '<?xml version="1.0" encoding="utf-8" ?>'
        '<D:propertyupdate xmlns:D="DAV:" '
        'xmlns:A="http://apple.com/ns/ical/" '
        'xmlns:C="urn:ietf:params:xml:ns:caldav">'
        "<D:set><D:prop>%s</D:prop></D:set>"
        "</D:propertyupdate>"
    ) % "".join(props)


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


def dav_make_calendar(
    base_url: str,
    username: str,
    password: str,
    calendar_home: str,
    slug: str,
    display_name: str = "",
    color: str = "",
    description: str = "",
    *,
    timeout_s: float = 20.0,
    verify_tls: bool = True,
) -> dict:
    """Crea una nueva coleccion de calendario CalDAV (MKCALENDAR + PROPPATCH).

    Crea la coleccion en `<calendar_home><slug>/` via MKCALENDAR, fijando el
    displayname en el propio cuerpo. Si se pasa `color` y/o `description`, hace
    un PROPPATCH posterior para setearlos. Idempotente: si la coleccion ya
    existe (405/301) devuelve {status:'ok', existed:True}.

    Args:
        base_url: URL base del servidor DAV (sin barra final), p.ej.
            'https://dav-x.organic-machine.com'.
        username: usuario para HTTP Basic auth.
        password: contrasena para HTTP Basic auth. Resolver desde pass.
        calendar_home: ruta del calendar-home del principal (con barra final),
            p.ej. '/enmanuel/calendars/'. La coleccion cuelga de el.
        slug: segmento de path de la coleccion (p.ej. 'trabajo'); se sanea a
            [a-z0-9_-]. Si queda vacio tras sanear, error de validacion.
        display_name: nombre visible (DAV:displayname). Si vacio, usa el slug.
        color: color de la coleccion como hex '#rrggbb' (Apple calendar-color).
            Opcional.
        description: descripcion (CalDAV calendar-description). Opcional.
        timeout_s: timeout de cada peticion en segundos. Default 20.0.
        verify_tls: si True (default) verifica el certificado TLS.

    Returns:
        dict. En exito: {status:'ok', http_status:int, href:str} (y existed:True
        si ya existia). En error (sin lanzar): {status:'error', http_status:
        int|None, href:str, error:str}.
    """
    clean = _sanitize_slug(slug)
    href = (calendar_home.rstrip("/") + "/" + clean + "/") if clean else ""
    if not clean:
        return {
            "status": "error",
            "http_status": None,
            "href": href,
            "error": "slug invalido: queda vacio tras sanear a [a-z0-9_-]",
        }

    name = display_name if display_name else clean
    url = _join_url(base_url, calendar_home, clean)
    context = None if verify_tls else ssl._create_unverified_context()
    headers = {
        "Authorization": _basic_auth_header(username, password),
        "Content-Type": "application/xml; charset=utf-8",
    }

    # 1) MKCALENDAR — crea la coleccion + displayname.
    mk_body = _build_mkcalendar_xml(name).encode("utf-8")
    req = urllib.request.Request(
        url, data=mk_body, method="MKCALENDAR", headers=headers
    )
    existed = False
    http_status = None
    try:
        with urllib.request.urlopen(req, timeout=timeout_s, context=context) as resp:
            http_status = resp.status
    except urllib.error.HTTPError as e:
        # 405/301: la coleccion ya existe en ese path -> idempotente.
        if e.code in (301, 405):
            existed = True
            http_status = e.code
        else:
            return {
                "status": "error",
                "http_status": e.code,
                "href": href,
                "error": "http %s" % e.code,
            }
    except urllib.error.URLError as e:
        return {
            "status": "error",
            "http_status": None,
            "href": href,
            "error": str(e.reason),
        }
    except Exception as e:  # noqa: BLE001
        return {"status": "error", "http_status": None, "href": href, "error": str(e)}

    # 2) PROPPATCH — color y/o descripcion. Fallo aqui no es fatal: la coleccion
    # ya existe. Servidores que no soporten calendar-color pueden ignorarlo.
    if color or description:
        pp_body = _build_proppatch_xml(color, description).encode("utf-8")
        pp_req = urllib.request.Request(
            url, data=pp_body, method="PROPPATCH", headers=headers
        )
        try:
            urllib.request.urlopen(pp_req, timeout=timeout_s, context=context).close()
        except Exception:  # noqa: BLE001
            # No fatal: el calendario quedo creado; el color/desc puede no
            # soportarse en este servidor. Se ignora silenciosamente.
            pass

    result = {"status": "ok", "http_status": http_status, "href": href}
    if existed:
        result["existed"] = True
    return result
