"""ICS (iCalendar) import/export provider (U8).

Uses the ``icalendar`` library for RFC 5545 compliant parsing/serialization.
Import delegates to ``calendar.db.insert_event`` for direct persistence with
``external_id`` set (so duplicate UIDs can be skipped on re-import).
Export reads via ``CalendarService.list_events``.
"""

from __future__ import annotations

import logging
import uuid
from datetime import date, datetime, timezone
from typing import Any

from icalendar import Calendar, Event
from icalendar.prop import vRecur

from agentkit.calendar.db import get_event_by_external_id, insert_event
from agentkit.calendar.models import CalendarEvent, _now_iso
from agentkit.calendar.service import CalendarService

logger = logging.getLogger(__name__)


def _to_iso_utc(dt: datetime) -> str:
    """Convert a datetime to ISO 8601 UTC string."""
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc).isoformat()


def _parse_iso(dt_str: str) -> datetime:
    """Parse an ISO 8601 string to a UTC-aware datetime."""
    dt = datetime.fromisoformat(dt_str)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


def _extract_dt(component: Any, key: str) -> tuple[str, bool]:
    """Extract a date/datetime property from an icalendar component.

    Returns ``(iso_string, is_all_day)``. ``is_all_day`` is True when the
    value is a bare ``date`` (not a ``datetime``).
    """
    prop = component.get(key)
    if prop is None:
        return "", False
    val = prop.dt
    if isinstance(val, date) and not isinstance(val, datetime):
        return val.isoformat(), True
    return _to_iso_utc(val), False


class ICSProvider:
    """Import/export calendar events as iCalendar (.ics) files."""

    def __init__(self, service: CalendarService) -> None:
        self.service = service

    async def import_ics(self, ics_bytes: bytes, user_id: str) -> dict[str, Any]:
        """Parse ICS bytes and create events for *user_id*.

        Returns ``{"imported": N, "skipped": M, "errors": [...]}``.
        Raises ``ValueError`` if the ICS content cannot be parsed at all.
        """
        # ponytail: hard size/count caps to prevent DoS via crafted ICS.
        # Upgrade: stream-parse for unbounded inputs.
        if len(ics_bytes) > 10 * 1024 * 1024:
            raise ValueError("ICS file too large (max 10MB)")

        imported = 0
        skipped = 0
        errors: list[str] = []

        try:
            cal = Calendar.from_ical(ics_bytes)
        except Exception as e:
            raise ValueError(f"Failed to parse ICS: {e}") from e

        components = list(cal.walk("VEVENT"))
        # ponytail: hard size/count caps to prevent DoS via crafted ICS.
        if len(components) > 10000:
            raise ValueError("Too many events in ICS file (max 10000)")

        for component in components:
            try:
                uid = str(component.get("UID", "") or "") or None

                # Dedup by (external_id, provider, user_id)
                if uid:
                    existing = await get_event_by_external_id(
                        uid, "ics", user_id, self.service.db_path
                    )
                    if existing is not None:
                        skipped += 1
                        continue

                title = str(component.get("SUMMARY", "") or "")
                if not title:
                    errors.append("VEVENT missing SUMMARY, skipped")
                    continue

                start_str, is_all_day = _extract_dt(component, "DTSTART")
                if not start_str:
                    errors.append("VEVENT missing DTSTART, skipped")
                    continue
                end_str, _ = _extract_dt(component, "DTEND")
                if not end_str:
                    end_str = start_str

                description = str(component.get("DESCRIPTION", "") or "")
                location = str(component.get("LOCATION", "") or "")

                rrule_str: str | None = None
                rrule = component.get("RRULE")
                if rrule is not None:
                    rrule_str = rrule.to_ical().decode("utf-8")

                now = _now_iso()
                event = CalendarEvent(
                    id=uuid.uuid4().hex,
                    user_id=user_id,
                    title=title,
                    description=description,
                    start_time=start_str,
                    end_time=end_str,
                    is_all_day=is_all_day,
                    location=location,
                    rrule=rrule_str,
                    source="manual",
                    external_id=uid,
                    external_provider="ics" if uid else None,
                    last_modified=now,
                    created_at=now,
                )
                await insert_event(event, self.service.db_path)
                imported += 1
            except Exception as e:
                errors.append(f"Failed to import VEVENT: {e}")
                logger.warning("ICS import error: %s", e)

        return {"imported": imported, "skipped": skipped, "errors": errors}

    def export_ics(self, events: list[CalendarEvent]) -> bytes:
        """Serialize *events* to ICS bytes."""
        cal = Calendar()
        cal.add("prodid", "-//Fischer AgentKit//Calendar//EN")
        cal.add("version", "2.0")

        # ponytail: list_events expands RRULE into occurrences (same event.id).
        # ICS wants one VEVENT with RRULE, not N copies — dedup by id.
        seen_ids: set[str] = set()
        for event in events:
            if event.id in seen_ids:
                continue
            seen_ids.add(event.id)
            cal.add_component(self._event_to_vevent(event))

        return cal.to_ical()

    def _event_to_vevent(self, event: CalendarEvent) -> Event:
        """Convert a :class:`CalendarEvent` to an icalendar ``Event`` component."""
        vevent = Event()
        vevent.add("uid", event.external_id or event.id)
        vevent.add("summary", event.title)

        start_dt = _parse_iso(event.start_time)
        end_dt = _parse_iso(event.end_time)

        if event.is_all_day:
            vevent.add("dtstart", start_dt.date())
            vevent.add("dtend", end_dt.date())
        else:
            vevent.add("dtstart", start_dt)
            vevent.add("dtend", end_dt)

        if event.description:
            vevent.add("description", event.description)
        if event.location:
            vevent.add("location", event.location)
        if event.rrule:
            # ponytail: vRecur.from_ical reorders keys (e.g. COUNT before BYDAY)
            # but the result is semantically equivalent RFC 5545.
            vevent.add("rrule", vRecur.from_ical(event.rrule))

        return vevent
