"""CalDAV calendar integration service."""

import asyncio
import logging
from datetime import date as date_type, datetime, timedelta
from zoneinfo import ZoneInfo

import caldav
import icalendar

from fabledassistant.services.settings import get_all_settings

logger = logging.getLogger(__name__)

CALDAV_SETTING_KEYS = ["caldav_url", "caldav_username", "caldav_password", "caldav_calendar_name", "caldav_timezone"]


async def get_caldav_config(user_id: int) -> dict[str, str]:
    """Return the user's CalDAV config from their settings."""
    all_settings = await get_all_settings(user_id)
    return {k: all_settings.get(k, "") for k in CALDAV_SETTING_KEYS}


async def is_caldav_configured(user_id: int) -> bool:
    """Check if the user has configured an external CalDAV server."""
    config = await get_caldav_config(user_id)
    return bool(config.get("caldav_url") and config.get("caldav_username") and config.get("caldav_password"))


def _get_calendar(client: caldav.DAVClient, calendar_name: str) -> caldav.Calendar:
    """Get a named calendar or the first available one (synchronous)."""
    principal = client.principal()
    calendars = principal.calendars()
    if not calendars:
        raise ValueError("No calendars found on the CalDAV server.")
    if calendar_name:
        for cal in calendars:
            if cal.name == calendar_name:
                return cal
        names = [c.name for c in calendars]
        raise ValueError(f"Calendar '{calendar_name}' not found. Available: {', '.join(names)}")
    return calendars[0]


def _get_all_calendars(client: caldav.DAVClient) -> list[caldav.Calendar]:
    """Get all calendars for the user (synchronous)."""
    principal = client.principal()
    calendars = principal.calendars()
    if not calendars:
        raise ValueError("No calendars found on the CalDAV server.")
    return calendars


def _make_client(config: dict[str, str]) -> caldav.DAVClient:
    """Create a CalDAV client from config dict."""
    return caldav.DAVClient(
        url=config["caldav_url"],
        username=config.get("caldav_username") or None,
        password=config.get("caldav_password") or None,
    )


def _parse_vevent(component) -> dict | None:
    """Extract event data from a VEVENT component."""
    if component.name != "VEVENT":
        return None
    title = str(component.get("SUMMARY", ""))
    dtstart = component.get("DTSTART")
    dtend = component.get("DTEND")
    location = str(component.get("LOCATION", ""))
    description = str(component.get("DESCRIPTION", ""))
    uid = str(component.get("UID", ""))
    start_str = dtstart.dt.isoformat() if dtstart else ""
    end_str = dtend.dt.isoformat() if dtend else ""

    result = {
        "uid": uid,
        "title": title,
        "start": start_str,
        "end": end_str,
        "location": location,
        "description": description,
    }

    # Extract recurrence rule
    rrule = component.get("RRULE")
    if rrule:
        result["recurrence"] = rrule.to_ical().decode("utf-8")

    # Extract alarms
    alarms = []
    for sub in component.subcomponents:
        if sub.name == "VALARM":
            trigger = sub.get("TRIGGER")
            if trigger and trigger.dt:
                minutes = abs(int(trigger.dt.total_seconds() // 60))
                alarms.append({"minutes_before": minutes})
    if alarms:
        result["alarms"] = alarms

    # Extract attendees
    attendees = component.get("ATTENDEE")
    if attendees:
        if not isinstance(attendees, list):
            attendees = [attendees]
        result["attendees"] = [str(a).replace("mailto:", "") for a in attendees]

    return result


def _parse_vtodo(component) -> dict | None:
    """Extract todo data from a VTODO component."""
    if component.name != "VTODO":
        return None
    uid = str(component.get("UID", ""))
    summary = str(component.get("SUMMARY", ""))
    description = str(component.get("DESCRIPTION", ""))
    status = str(component.get("STATUS", ""))
    due = component.get("DUE")
    due_str = due.dt.isoformat() if due else ""
    priority = component.get("PRIORITY")
    priority_val = int(priority) if priority else None

    return {
        "uid": uid,
        "summary": summary,
        "description": description,
        "due": due_str,
        "status": status,
        "priority": priority_val,
    }


def _apply_timezone(dt: datetime, timezone: str | None) -> datetime:
    """Apply a timezone to a naive datetime. Returns dt unchanged if already aware."""
    if dt.tzinfo is not None:
        return dt
    if timezone:
        return dt.replace(tzinfo=ZoneInfo(timezone))
    return dt


def _build_valarm(minutes_before: int) -> icalendar.Alarm:
    """Create a DISPLAY alarm component triggered N minutes before the event."""
    alarm = icalendar.Alarm()
    alarm.add("action", "DISPLAY")
    alarm.add("description", "Reminder")
    alarm.add("trigger", timedelta(minutes=-minutes_before))
    return alarm


def _add_attendees(event: icalendar.Event, attendees: list[str]) -> None:
    """Add mailto: attendees to an iCalendar event."""
    for email in attendees:
        attendee = icalendar.vCalAddress(f"mailto:{email}")
        event.add("attendee", attendee)


def _check_config(config: dict[str, str]) -> None:
    """Raise if CalDAV is not configured."""
    if not config.get("caldav_url"):
        raise ValueError("CalDAV is not configured. Go to Settings → Calendar to enter your server URL.")


async def create_event(
    user_id: int,
    title: str,
    start: str,
    end: str | None = None,
    duration: int | None = None,
    description: str | None = None,
    location: str | None = None,
    all_day: bool = False,
    recurrence: str | None = None,
    timezone: str | None = None,
    reminder_minutes: int | None = None,
    attendees: list[str] | None = None,
    calendar_name: str | None = None,
    uid: str | None = None,
) -> dict:
    """Create a calendar event.

    start/end are ISO date (YYYY-MM-DD) or datetime strings.
    If all_day is True, DTSTART/DTEND use DATE values.
    recurrence is an iCalendar RRULE string (e.g. "FREQ=YEARLY").
    """
    config = await get_caldav_config(user_id)
    _check_config(config)

    tz = timezone or config.get("caldav_timezone") or None

    cal = icalendar.Calendar()
    cal.add("prodid", "-//FabledAssistant//EN")
    cal.add("version", "2.0")

    event = icalendar.Event()
    if uid:
        # Remove auto-generated UID if the library added one, then inject ours
        if "UID" in event:
            del event["UID"]
        event.add("uid", uid)
    event.add("summary", title)

    if all_day:
        # All-day events use DATE values (no time component)
        d_start = datetime.fromisoformat(start).date() if "T" in start else date_type.fromisoformat(start)
        if end:
            d_end = datetime.fromisoformat(end).date() if "T" in end else date_type.fromisoformat(end)
        else:
            d_end = d_start + timedelta(days=1)
        event.add("dtstart", d_start)
        event.add("dtend", d_end)
        result_start = d_start.isoformat()
        result_end = d_end.isoformat()
    else:
        dt_start = _apply_timezone(datetime.fromisoformat(start), tz)
        if end:
            dt_end = _apply_timezone(datetime.fromisoformat(end), tz)
        else:
            dt_end = dt_start + timedelta(minutes=duration or 60)
        event.add("dtstart", dt_start)
        event.add("dtend", dt_end)
        result_start = dt_start.isoformat()
        result_end = dt_end.isoformat()

    if description:
        event.add("description", description)
    if location:
        event.add("location", location)
    if recurrence:
        # Parse RRULE string like "FREQ=YEARLY" into a vRecur dict
        rrule_parts = {}
        for part in recurrence.split(";"):
            if "=" in part:
                key, value = part.split("=", 1)
                rrule_parts[key.strip().lower()] = value.strip()
        event.add("rrule", rrule_parts)
    if reminder_minutes is not None:
        event.add_component(_build_valarm(reminder_minutes))
    if attendees:
        _add_attendees(event, attendees)

    cal.add_component(event)

    ical_str = cal.to_ical().decode("utf-8")

    def _save():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        calendar = _get_calendar(client, cal_name)
        calendar.save_event(ical_str)

    await asyncio.to_thread(_save)

    result = {
        "title": title,
        "start": result_start,
        "end": result_end,
        "all_day": all_day,
    }
    if recurrence:
        result["recurrence"] = recurrence
    return result


async def list_events(user_id: int, date_from: str, date_to: str) -> list[dict]:
    """List calendar events in a date range. Dates are ISO datetime strings.

    Searches all calendars unless caldav_calendar_name is configured.
    """
    config = await get_caldav_config(user_id)
    _check_config(config)

    dt_from = datetime.fromisoformat(date_from)
    dt_to = datetime.fromisoformat(date_to)

    def _search():
        client = _make_client(config)
        cal_name = config.get("caldav_calendar_name", "")
        if cal_name:
            calendars = [_get_calendar(client, cal_name)]
        else:
            calendars = _get_all_calendars(client)
        all_results = []
        for calendar in calendars:
            try:
                all_results.extend(calendar.date_search(dt_from, dt_to))
            except Exception:
                logger.warning("Failed to search calendar '%s'", getattr(calendar, 'name', '?'))
        return all_results

    results = await asyncio.to_thread(_search)

    events = []
    for result in results:
        cal = icalendar.Calendar.from_ical(result.data)
        for component in cal.walk():
            parsed = _parse_vevent(component)
            if parsed:
                events.append(parsed)
    return events


async def search_events(user_id: int, query: str, days_ahead: int = 90) -> list[dict]:
    """Search events by keyword in the next N days."""
    now = datetime.now()
    date_from = now.isoformat()
    date_to = (now + timedelta(days=days_ahead)).isoformat()

    all_events = await list_events(user_id, date_from, date_to)
    q = query.lower()
    return [
        e for e in all_events
        if q in e["title"].lower() or q in e.get("location", "").lower() or q in e.get("description", "").lower()
    ]


async def update_event(
    user_id: int,
    query: str,
    title: str | None = None,
    start: str | None = None,
    end: str | None = None,
    description: str | None = None,
    location: str | None = None,
    timezone: str | None = None,
    calendar_name: str | None = None,
) -> dict:
    """Update a calendar event matching the query."""
    config = await get_caldav_config(user_id)
    _check_config(config)
    tz = timezone or config.get("caldav_timezone") or None

    def _do_update():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        now = datetime.now()
        if cal_name:
            calendars = [_get_calendar(client, cal_name)]
        else:
            calendars = _get_all_calendars(client)
        results = []
        for cal in calendars:
            try:
                results.extend(cal.date_search(now - timedelta(days=30), now + timedelta(days=365)))
            except Exception:
                logger.warning("Failed to search calendar '%s'", getattr(cal, 'name', '?'))

        q = query.lower()
        matches = []
        for r in results:
            cal_obj = icalendar.Calendar.from_ical(r.data)
            for component in cal_obj.walk():
                if component.name == "VEVENT":
                    event_title = str(component.get("SUMMARY", ""))
                    if q in event_title.lower():
                        matches.append((r, component))

        if not matches:
            raise ValueError(f"No event found matching '{query}'.")
        if len(matches) > 3:
            titles = [str(m[1].get("SUMMARY", "")) for m in matches]
            raise ValueError(f"Too many matches ({len(matches)}) for '{query}'. Be more specific. Found: {', '.join(titles[:10])}")

        event_obj, component = matches[0]
        if title:
            component["SUMMARY"] = title
        if start:
            dt_start = _apply_timezone(datetime.fromisoformat(start), tz)
            del component["DTSTART"]
            component.add("dtstart", dt_start)
        if end:
            dt_end = _apply_timezone(datetime.fromisoformat(end), tz)
            if "DTEND" in component:
                del component["DTEND"]
            component.add("dtend", dt_end)
        if description is not None:
            if "DESCRIPTION" in component:
                del component["DESCRIPTION"]
            component.add("description", description)
        if location is not None:
            if "LOCATION" in component:
                del component["LOCATION"]
            component.add("location", location)

        # Rebuild ical data and save
        cal_data = icalendar.Calendar()
        cal_data.add("prodid", "-//FabledAssistant//EN")
        cal_data.add("version", "2.0")
        cal_data.add_component(component)
        event_obj.data = cal_data.to_ical().decode("utf-8")
        event_obj.save()

        return _parse_vevent(component)

    return await asyncio.to_thread(_do_update)


async def delete_event(
    user_id: int,
    query: str,
    calendar_name: str | None = None,
) -> dict:
    """Delete a calendar event matching the query."""
    config = await get_caldav_config(user_id)
    _check_config(config)

    def _do_delete():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        now = datetime.now()
        if cal_name:
            calendars = [_get_calendar(client, cal_name)]
        else:
            calendars = _get_all_calendars(client)
        results = []
        for cal in calendars:
            try:
                results.extend(cal.date_search(now - timedelta(days=30), now + timedelta(days=365)))
            except Exception:
                logger.warning("Failed to search calendar '%s'", getattr(cal, 'name', '?'))

        q = query.lower()
        matches = []
        for r in results:
            cal_obj = icalendar.Calendar.from_ical(r.data)
            for component in cal_obj.walk():
                if component.name == "VEVENT":
                    event_title = str(component.get("SUMMARY", ""))
                    if q in event_title.lower():
                        matches.append((r, component))

        if not matches:
            raise ValueError(f"No event found matching '{query}'.")
        if len(matches) > 3:
            titles = [str(m[1].get("SUMMARY", "")) for m in matches]
            raise ValueError(f"Too many matches ({len(matches)}) for '{query}'. Be more specific. Found: {', '.join(titles[:10])}")

        event_obj, component = matches[0]
        parsed = _parse_vevent(component)
        event_obj.delete()
        return parsed

    return await asyncio.to_thread(_do_delete)


async def list_calendars(user_id: int) -> list[dict]:
    """List all calendars for the user."""
    config = await get_caldav_config(user_id)
    _check_config(config)

    def _list():
        client = _make_client(config)
        principal = client.principal()
        calendars = principal.calendars()
        return [{"name": c.name, "url": str(c.url)} for c in calendars]

    return await asyncio.to_thread(_list)


async def create_todo(
    user_id: int,
    summary: str,
    due: str | None = None,
    description: str | None = None,
    priority: int | None = None,
    reminder_minutes: int | None = None,
    timezone: str | None = None,
    calendar_name: str | None = None,
) -> dict:
    """Create a CalDAV todo (VTODO)."""
    config = await get_caldav_config(user_id)
    _check_config(config)
    tz = timezone or config.get("caldav_timezone") or None

    def _create():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        calendar = _get_calendar(client, cal_name)

        kwargs = {"summary": summary}
        if due:
            dt_due = datetime.fromisoformat(due)
            dt_due = _apply_timezone(dt_due, tz)
            kwargs["due"] = dt_due

        todo = calendar.save_todo(**kwargs)

        # Modify component for extra fields
        cal_obj = icalendar.Calendar.from_ical(todo.data)
        modified = False
        for component in cal_obj.walk():
            if component.name == "VTODO":
                if description:
                    component.add("description", description)
                    modified = True
                if priority is not None:
                    component.add("priority", priority)
                    modified = True
                if reminder_minutes is not None:
                    component.add_component(_build_valarm(reminder_minutes))
                    modified = True
                if modified:
                    todo.data = cal_obj.to_ical().decode("utf-8")
                    todo.save()
                return _parse_vtodo(component)

        return {"summary": summary}

    return await asyncio.to_thread(_create)


async def list_todos(
    user_id: int,
    include_completed: bool = False,
    calendar_name: str | None = None,
) -> list[dict]:
    """List CalDAV todos."""
    config = await get_caldav_config(user_id)
    _check_config(config)

    def _list():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        calendar = _get_calendar(client, cal_name)
        todos = calendar.todos(include_completed=include_completed)
        results = []
        for t in todos:
            cal_obj = icalendar.Calendar.from_ical(t.data)
            for component in cal_obj.walk():
                parsed = _parse_vtodo(component)
                if parsed:
                    results.append(parsed)
        return results

    return await asyncio.to_thread(_list)


async def search_todos(
    user_id: int,
    query: str,
    include_completed: bool = False,
    calendar_name: str | None = None,
) -> list[dict]:
    """Search CalDAV todos by keyword in summary or description."""
    todos = await list_todos(user_id, include_completed=include_completed, calendar_name=calendar_name)
    q = query.lower()
    return [
        t for t in todos
        if q in t.get("summary", "").lower() or q in (t.get("description") or "").lower()
    ]


async def complete_todo(
    user_id: int,
    query: str,
    calendar_name: str | None = None,
) -> dict:
    """Complete a CalDAV todo matching the query."""
    config = await get_caldav_config(user_id)
    _check_config(config)

    def _complete():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        calendar = _get_calendar(client, cal_name)
        todos = calendar.todos(include_completed=False)

        q = query.lower()
        matches = []
        for t in todos:
            cal_obj = icalendar.Calendar.from_ical(t.data)
            for component in cal_obj.walk():
                if component.name == "VTODO":
                    s = str(component.get("SUMMARY", ""))
                    if q in s.lower():
                        matches.append((t, component))

        if not matches:
            raise ValueError(f"No todo found matching '{query}'.")
        if len(matches) > 3:
            titles = [str(m[1].get("SUMMARY", "")) for m in matches]
            raise ValueError(f"Too many matches ({len(matches)}) for '{query}'. Be more specific. Found: {', '.join(titles[:10])}")

        todo_obj, component = matches[0]
        todo_obj.complete()

        # Re-parse after completing
        cal_obj = icalendar.Calendar.from_ical(todo_obj.data)
        for comp in cal_obj.walk():
            parsed = _parse_vtodo(comp)
            if parsed:
                return parsed
        return {"summary": str(component.get("SUMMARY", "")), "status": "COMPLETED"}

    return await asyncio.to_thread(_complete)


async def update_todo(
    user_id: int,
    query: str,
    summary: str | None = None,
    due: str | None = None,
    description: str | None = None,
    priority: int | None = None,
    timezone: str | None = None,
    calendar_name: str | None = None,
) -> dict:
    """Update a CalDAV todo matching the query."""
    config = await get_caldav_config(user_id)
    _check_config(config)
    tz = timezone or config.get("caldav_timezone") or None

    def _do_update():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        calendar = _get_calendar(client, cal_name)
        todos = calendar.todos(include_completed=True)

        q = query.lower()
        matches = []
        for t in todos:
            cal_obj = icalendar.Calendar.from_ical(t.data)
            for component in cal_obj.walk():
                if component.name == "VTODO":
                    s = str(component.get("SUMMARY", ""))
                    if q in s.lower():
                        matches.append((t, component))

        if not matches:
            raise ValueError(f"No todo found matching '{query}'.")
        if len(matches) > 3:
            titles = [str(m[1].get("SUMMARY", "")) for m in matches]
            raise ValueError(
                f"Too many matches ({len(matches)}) for '{query}'. "
                f"Be more specific. Found: {', '.join(titles[:10])}"
            )

        todo_obj, component = matches[0]

        if summary:
            component["SUMMARY"] = summary
        if description is not None:
            if "DESCRIPTION" in component:
                del component["DESCRIPTION"]
            component.add("description", description)
        if priority is not None:
            if "PRIORITY" in component:
                del component["PRIORITY"]
            component.add("priority", priority)
        if due:
            if "DUE" in component:
                del component["DUE"]
            try:
                dt = datetime.fromisoformat(due)
                dt = _apply_timezone(dt, tz)
                component.add("due", dt)
            except ValueError:
                component.add("due", date_type.fromisoformat(due))

        # Rebuild ical data and save
        cal_data = icalendar.Calendar()
        cal_data.add("prodid", "-//FabledAssistant//EN")
        cal_data.add("version", "2.0")
        cal_data.add_component(component)
        todo_obj.data = cal_data.to_ical().decode("utf-8")
        todo_obj.save()

        return _parse_vtodo(component)

    return await asyncio.to_thread(_do_update)


async def delete_todo(
    user_id: int,
    query: str,
    calendar_name: str | None = None,
) -> dict:
    """Delete a CalDAV todo matching the query."""
    config = await get_caldav_config(user_id)
    _check_config(config)

    def _delete():
        client = _make_client(config)
        cal_name = calendar_name or config.get("caldav_calendar_name", "")
        calendar = _get_calendar(client, cal_name)
        todos = calendar.todos(include_completed=True)

        q = query.lower()
        matches = []
        for t in todos:
            cal_obj = icalendar.Calendar.from_ical(t.data)
            for component in cal_obj.walk():
                if component.name == "VTODO":
                    s = str(component.get("SUMMARY", ""))
                    if q in s.lower():
                        matches.append((t, component))

        if not matches:
            raise ValueError(f"No todo found matching '{query}'.")
        if len(matches) > 3:
            titles = [str(m[1].get("SUMMARY", "")) for m in matches]
            raise ValueError(f"Too many matches ({len(matches)}) for '{query}'. Be more specific. Found: {', '.join(titles[:10])}")

        todo_obj, component = matches[0]
        parsed = _parse_vtodo(component)
        todo_obj.delete()
        return parsed

    return await asyncio.to_thread(_delete)


async def test_connection(user_id: int) -> dict:
    """Test the CalDAV connection and return status."""
    config = await get_caldav_config(user_id)
    if not config.get("caldav_url"):
        return {"success": False, "error": "CalDAV is not configured."}

    def _test():
        client = _make_client(config)
        principal = client.principal()
        calendars = principal.calendars()
        return [c.name for c in calendars]

    try:
        calendar_names = await asyncio.to_thread(_test)
        return {
            "success": True,
            "calendars": calendar_names,
            "message": f"Connected successfully. Found {len(calendar_names)} calendar(s).",
        }
    except Exception as e:
        error_msg = str(e)
        if "401" in error_msg or "403" in error_msg or "Unauthorized" in error_msg:
            error_msg = "Authentication failed. Check your username and password."
        elif "404" in error_msg or "Not Found" in error_msg:
            error_msg = "CalDAV endpoint not found. Check your URL."
        elif "Connection" in error_msg or "resolve" in error_msg:
            error_msg = f"Connection failed: {error_msg}"
        return {"success": False, "error": error_msg}
