"""Serializa un evento (dict) a un VCALENDAR completo con un VEVENT dentro.

Analoga de ``build_vcard`` pero para calendarios: compone un texto iCalendar
(RFC 5545) con un envoltorio VCALENDAR y un unico VEVENT. Es una funcion pura —
solo compone texto, sin red, sin disco y sin reloj (nunca usa ``datetime.now``).
La unica excepcion posible es ``ValueError`` por validacion de entrada (falta de
``summary`` o ``start``), lo cual es aceptable para una funcion pura, en paridad
con ``build_vcard``.

Acepta claves en espanol e ingles. Las fechas se aceptan en varios formatos
humanos y se normalizan al formato iCal compacto. Si falta ``uid``, se sintetiza
un UID determinista (md5 de summary+start) para que el mismo evento produzca
siempre el mismo recurso (idempotencia al subir a CalDAV).
"""

import hashlib
from datetime import datetime, timedelta

_PRODID = "-//fn_registry//build_vevent//ES"


def _ical_escape(value: str) -> str:
    """Escapa un valor de texto para una linea iCal (RFC 5545).

    Reglas: ``\\`` -> ``\\\\``, salto de linea -> ``\\n``, ``,`` -> ``\\,``,
    ``;`` -> ``\\;``. El retorno de carro ``\\r`` crudo se ELIMINA (no se escapa),
    mismo criterio que el escape vCard de ``build_vcard``: un ``\\r`` solo sin
    ``\\n`` que lo siga sobreviviria al escape de ``\\n`` y quedaria como caracter
    de control capaz de inyectar propiedades nuevas. Eliminarlo cierra ese vector.
    """
    return (
        value.replace("\\", "\\\\")
        .replace("\r", "")
        .replace("\n", "\\n")
        .replace(",", "\\,")
        .replace(";", "\\;")
    )


def _pick(event: dict, *keys):
    """Devuelve el primer valor no vacio entre ``keys`` (acepta ES/EN)."""
    for key in keys:
        val = event.get(key)
        if val:
            return val
    return None


def _is_compact_datetime(s: str) -> bool:
    """True si ``s`` ya viene en formato iCal compacto (YYYYMMDDTHHMMSS[Z])."""
    body = s[:-1] if s.endswith("Z") else s
    if "T" not in body:
        # Posible fecha compacta YYYYMMDD.
        return len(body) == 8 and body.isdigit()
    date_part, _, time_part = body.partition("T")
    return (
        len(date_part) == 8
        and date_part.isdigit()
        and len(time_part) == 6
        and time_part.isdigit()
    )


def _is_compact_date(s: str) -> bool:
    """True si ``s`` es una fecha iCal compacta YYYYMMDD (sin hora)."""
    return len(s) == 8 and s.isdigit()


def _parse_date(value: str) -> str:
    """Normaliza una fecha (sin hora) a iCal compacto YYYYMMDD.

    Acepta 'YYYY-MM-DD', 'YYYYMMDD', o un datetime humano del que toma la fecha.
    """
    s = str(value).strip()
    if _is_compact_date(s):
        return s
    if _is_compact_datetime(s):
        # Tiene hora pero se pidio all_day: quedarse con la parte de fecha.
        return s[:8]
    # Formato con guiones, posiblemente con hora.
    if "T" in s:
        s = s.split("T", 1)[0]
    dt = datetime.strptime(s, "%Y-%m-%d")
    return dt.strftime("%Y%m%d")


def _parse_datetime(value: str) -> str:
    """Normaliza una fecha/hora a iCal compacto YYYYMMDDTHHMMSS[Z].

    Acepta:
      - '2026-06-20T17:00' o '2026-06-20T17:00:00' (naive local)
      - '2026-06-20T17:00:00Z' o '2026-06-20T17:00Z' (UTC, sufijo Z)
      - '2026-06-20' (solo fecha -> medianoche local)
      - '20260620T170000' / '20260620T170000Z' (ya compacto, se respeta)
    Nunca usa el reloj del sistema: la conversion es determinista.
    """
    s = str(value).strip()
    if _is_compact_datetime(s):
        return s
    if _is_compact_date(s):
        return s + "T000000"

    utc = s.endswith("Z")
    if utc:
        s = s[:-1]

    if "T" in s:
        try:
            dt = datetime.strptime(s, "%Y-%m-%dT%H:%M:%S")
        except ValueError:
            dt = datetime.strptime(s, "%Y-%m-%dT%H:%M")
    else:
        # Solo fecha -> medianoche.
        dt = datetime.strptime(s, "%Y-%m-%d")

    compact = dt.strftime("%Y%m%dT%H%M%S")
    return compact + "Z" if utc else compact


def _next_day_compact(date_compact: str) -> str:
    """Dada una fecha iCal compacta YYYYMMDD devuelve la del dia siguiente."""
    dt = datetime.strptime(date_compact, "%Y%m%d") + timedelta(days=1)
    return dt.strftime("%Y%m%d")


def _plus_one_hour(dt_compact: str) -> str:
    """Dada una datetime iCal compacta devuelve la misma +1h preservando Z."""
    utc = dt_compact.endswith("Z")
    body = dt_compact[:-1] if utc else dt_compact
    dt = datetime.strptime(body, "%Y%m%dT%H%M%S") + timedelta(hours=1)
    out = dt.strftime("%Y%m%dT%H%M%S")
    return out + "Z" if utc else out


def build_vevent(event: dict) -> str:
    """Serializa un evento (dict) a un VCALENDAR completo con un VEVENT.

    Args:
        event: dict del evento. Claves opcionales salvo lo indicado (acepta
            nombre ES o EN):
            - ``uid``: identificador del evento. Si falta, se sintetiza
              determinista a partir de summary+start: '<evt->md5(...)[:16]>'.
            - ``summary`` / ``titulo`` / ``resumen``: -> SUMMARY (OBLIGATORIO).
            - ``start`` / ``inicio``: fecha/hora de inicio -> DTSTART (OBLIGATORIO).
            - ``end`` / ``fin``: fecha/hora de fin -> DTEND. Si falta y no es
              all_day, se deriva +1h del start; si es all_day, el dia siguiente.
            - ``all_day`` / ``todo_el_dia`` (bool): si True emite
              DTSTART;VALUE=DATE:YYYYMMDD (y DTEND como fecha siguiente).
            - ``location`` / ``ubicacion`` / ``lugar``: -> LOCATION.
            - ``description`` / ``descripcion`` / ``notas``: -> DESCRIPTION.
            - ``rrule`` / ``recurrencia``: string RRULE -> linea RRULE:...
            - ``dtstamp``: timestamp iCal opcional. Si falta, se usa el valor de
              DTSTART como fallback DETERMINISTA (nunca datetime.now). DTSTAMP es
              obligatorio en RFC 5545, por eso siempre se emite.
            - ``alarm_minutes`` / ``recordatorio_min`` (int): si presente, anade
              un bloque VALARM (display) con TRIGGER:-PT<N>M (N minutos antes).

    Returns:
        Texto VCALENDAR (RFC 5545) con lineas separadas por CRLF, empezando en
        BEGIN:VCALENDAR / VERSION:2.0 / PRODID / CALSCALE:GREGORIAN, conteniendo
        un VEVENT, y terminando en ``END:VCALENDAR\\r\\n``. Valores de texto
        escapados segun RFC 5545.

    Raises:
        ValueError: si falta ``summary`` o ``start`` (sin estos no hay evento).
    """
    summary = _pick(event, "summary", "titulo", "resumen")
    if not summary:
        raise ValueError("build_vevent: falta summary (titulo/resumen)")
    summary = str(summary).strip()

    start_raw = _pick(event, "start", "inicio")
    if not start_raw:
        raise ValueError("build_vevent: falta start (inicio)")

    all_day = bool(event.get("all_day") or event.get("todo_el_dia"))

    if all_day:
        dtstart = _parse_date(start_raw)
        end_raw = _pick(event, "end", "fin")
        dtend = _parse_date(end_raw) if end_raw else _next_day_compact(dtstart)
    else:
        dtstart = _parse_datetime(start_raw)
        end_raw = _pick(event, "end", "fin")
        dtend = _parse_datetime(end_raw) if end_raw else _plus_one_hour(dtstart)

    # UID: explicito o sintetico determinista (md5 de summary+start crudo).
    uid = event.get("uid")
    if uid:
        uid = str(uid).strip()
    else:
        digest = hashlib.md5(
            ("%s%s" % (summary, dtstart)).encode("utf-8")
        ).hexdigest()[:16]
        uid = "evt-%s" % digest

    # DTSTAMP: explicito o fallback determinista al DTSTART.
    dtstamp = event.get("dtstamp")
    dtstamp = str(dtstamp).strip() if dtstamp else dtstart

    lines = [
        "BEGIN:VCALENDAR",
        "VERSION:2.0",
        "PRODID:%s" % _PRODID,
        "CALSCALE:GREGORIAN",
        "BEGIN:VEVENT",
        "UID:%s" % _ical_escape(uid),
        "DTSTAMP:%s" % dtstamp,
    ]

    if all_day:
        lines.append("DTSTART;VALUE=DATE:%s" % dtstart)
        lines.append("DTEND;VALUE=DATE:%s" % dtend)
    else:
        lines.append("DTSTART:%s" % dtstart)
        lines.append("DTEND:%s" % dtend)

    lines.append("SUMMARY:%s" % _ical_escape(summary))

    location = _pick(event, "location", "ubicacion", "lugar")
    if location:
        lines.append("LOCATION:%s" % _ical_escape(str(location)))

    description = _pick(event, "description", "descripcion", "notas")
    if description:
        lines.append("DESCRIPTION:%s" % _ical_escape(str(description)))

    rrule = _pick(event, "rrule", "recurrencia")
    if rrule:
        # RRULE es un valor estructurado (FREQ=...;BYDAY=...): NO se escapa el
        # contenido, sus ';' y ',' son separadores propios del recurrence rule.
        lines.append("RRULE:%s" % str(rrule).strip())

    alarm_minutes = _pick(event, "alarm_minutes", "recordatorio_min")
    if alarm_minutes:
        minutes = int(alarm_minutes)
        lines.append("BEGIN:VALARM")
        lines.append("ACTION:DISPLAY")
        lines.append("DESCRIPTION:%s" % _ical_escape(summary))
        lines.append("TRIGGER:-PT%dM" % minutes)
        lines.append("END:VALARM")

    lines.append("END:VEVENT")
    lines.append("END:VCALENDAR")
    return "\r\n".join(lines) + "\r\n"
