# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Calendar Skill

Manage calendar events via Google Calendar API or CalDAV (Nextcloud, Radicale, etc.).

Google Setup:
1. Create OAuth credentials at https://console.cloud.google.com
2. Enable Calendar API
3. Download credentials.json to ~/.familiar/data/google_credentials.json
4. First run will prompt for authorization

CalDAV Setup:
  CALDAV_URL=https://cloud.example.org/remote.php/dav/calendars/alice/
  CALDAV_USER=alice
  CALDAV_PASSWORD=app-password
  CALENDAR_PROVIDER=caldav   (optional — auto-detected if omitted)
"""

import importlib.util
import logging
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path

logger = logging.getLogger(__name__)


REQUEST_TIMEOUT = 30  # seconds for API calls

# Storage paths - use centralized paths


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.paths import DATA_DIR
except ImportError:
    DATA_DIR = _get_data_dir()

CREDENTIALS_FILE = DATA_DIR / "google_credentials.json"
TOKEN_FILE = DATA_DIR / "google_token.json"

# Scopes needed
SCOPES = ["https://www.googleapis.com/auth/calendar"]


# ---------------------------------------------------------------------------
# Provider detection
# ---------------------------------------------------------------------------


def _google_available() -> bool:
    """True if Google Calendar libs are importable and token file exists."""
    if not TOKEN_FILE.exists():
        return False
    for mod in ("google.oauth2", "googleapiclient", "httplib2"):
        try:
            if importlib.util.find_spec(mod) is None:
                return False
        except (ModuleNotFoundError, ValueError):
            return False
    return True


def _caldav_available() -> bool:
    """True if caldav+vobject are importable and CalDAV credentials are set."""
    for mod in ("caldav", "vobject"):
        try:
            if importlib.util.find_spec(mod) is None:
                return False
        except (ModuleNotFoundError, ValueError):
            return False
    return bool(
        os.environ.get("CALDAV_URL")
        and os.environ.get("CALDAV_USER")
        and os.environ.get("CALDAV_PASSWORD")
    )


def _calendar_provider() -> str:
    """Determine which calendar backend to use.

    Returns "google", "caldav", or "none".
    Checks CALENDAR_PROVIDER env var first, then auto-detects.
    """
    explicit = os.environ.get("CALENDAR_PROVIDER", "").lower().strip()
    if explicit == "google" and _google_available():
        return "google"
    if explicit == "caldav" and _caldav_available():
        return "caldav"
    if explicit and explicit not in ("google", "caldav"):
        logger.warning(f"Unknown CALENDAR_PROVIDER '{explicit}', auto-detecting")

    # Auto-detect: prefer Google (existing default) when both available
    if _google_available():
        return "google"
    if _caldav_available():
        return "caldav"
    return "none"


_NOT_CONFIGURED_MSG = "Calendar not configured. Use /connect calendar to set up Google or CalDAV."


# ---------------------------------------------------------------------------
# Google Calendar backend (existing code, unchanged)
# ---------------------------------------------------------------------------


def _get_calendar_service():
    """Get authenticated Google Calendar service."""
    try:
        from google.auth.transport.requests import Request
        from google.oauth2.credentials import Credentials
        from google_auth_oauthlib.flow import InstalledAppFlow
        from googleapiclient.discovery import build
    except ImportError:
        from familiar.core.deps import GOOGLE_PACKAGES, ensure_packages

        ok, _ = ensure_packages(GOOGLE_PACKAGES)
        if ok:
            from google.auth.transport.requests import Request
            from google.oauth2.credentials import Credentials
            from google_auth_oauthlib.flow import InstalledAppFlow
            from googleapiclient.discovery import build
        else:
            raise RuntimeError("Google API setup failed. Check server logs.")

    if not CREDENTIALS_FILE.exists():
        raise RuntimeError(
            f"Google credentials not found at {CREDENTIALS_FILE}\n"
            "Download from Google Cloud Console (OAuth 2.0 Client ID)"
        )

    creds = None

    # Load existing token
    if TOKEN_FILE.exists():
        creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES)

    # Refresh or get new credentials
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(str(CREDENTIALS_FILE), SCOPES)
            creds = flow.run_local_server(port=0)

        # Save token
        TOKEN_FILE.write_text(creds.to_json())

    import httplib2

    http = httplib2.Http(timeout=REQUEST_TIMEOUT)
    http = creds.authorize(http)
    return build("calendar", "v3", http=http)


# ---------------------------------------------------------------------------
# CalDAV backend (adapted from nextcloud/skill.py)
# ---------------------------------------------------------------------------


def _get_caldav_client():
    """Return a caldav.DAVClient connected to the configured CalDAV server."""
    try:
        import caldav
    except ImportError:
        from familiar.core.deps import CALDAV_PACKAGES, ensure_packages

        ok, _ = ensure_packages(CALDAV_PACKAGES)
        if ok:
            import caldav
        else:
            raise RuntimeError("CalDAV setup failed. Check server logs.")

    url = os.environ.get("CALDAV_URL", "")
    user = os.environ.get("CALDAV_USER", "")
    password = os.environ.get("CALDAV_PASSWORD", "")
    if not (url and user and password):
        raise RuntimeError("CalDAV not configured. Set CALDAV_URL, CALDAV_USER, CALDAV_PASSWORD.")
    return caldav.DAVClient(url=url, username=user, password=password)


def _get_caldav_client_for_account(account: dict):
    """Return a caldav.DAVClient for a specific account dict."""
    try:
        import caldav
    except ImportError:
        from familiar.core.deps import CALDAV_PACKAGES, ensure_packages

        ok, _ = ensure_packages(CALDAV_PACKAGES)
        if ok:
            import caldav
        else:
            raise RuntimeError("CalDAV setup failed. Check server logs.")
    return caldav.DAVClient(
        url=account["url"], username=account["user"], password=account["password"]
    )


def _get_google_service_for_account(account: dict):
    """Returns (service, calendar_id) tuple for a specific Google account."""
    try:
        from google.auth.transport.requests import Request
        from google.oauth2.credentials import Credentials
        from google_auth_oauthlib.flow import InstalledAppFlow
        from googleapiclient.discovery import build
    except ImportError:
        from familiar.core.deps import GOOGLE_PACKAGES, ensure_packages

        ok, _ = ensure_packages(GOOGLE_PACKAGES)
        if ok:
            from google.auth.transport.requests import Request
            from google.oauth2.credentials import Credentials
            from google_auth_oauthlib.flow import InstalledAppFlow
            from googleapiclient.discovery import build
        else:
            raise RuntimeError("Google API setup failed. Check server logs.")

    data_dir = _get_data_dir()
    token_path = data_dir / account.get("token_file", "google_token.json")
    calendar_id = account.get("calendar_id", "primary")

    if not CREDENTIALS_FILE.exists():
        raise RuntimeError(
            f"Google credentials not found at {CREDENTIALS_FILE}\n"
            "Download from Google Cloud Console (OAuth 2.0 Client ID)"
        )

    creds = None
    if token_path.exists():
        creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)

    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(str(CREDENTIALS_FILE), SCOPES)
            creds = flow.run_local_server(port=0)
        token_path.write_text(creds.to_json())

    import httplib2

    http = httplib2.Http(timeout=REQUEST_TIMEOUT)
    http = creds.authorize(http)
    service = build("calendar", "v3", http=http)
    return service, calendar_id


def _parse_vevent(cal_data: str) -> list:
    """Parse VCALENDAR data and return list of event dicts."""
    try:
        import vobject
    except ImportError:
        return []
    events = []
    try:
        for cal in vobject.readComponents(cal_data):
            for vevent in cal.contents.get("vevent", []):
                ev = {}
                if hasattr(vevent, "summary"):
                    ev["title"] = str(vevent.summary.value)
                if hasattr(vevent, "dtstart"):
                    ev["start"] = vevent.dtstart.value
                if hasattr(vevent, "dtend"):
                    ev["end"] = vevent.dtend.value
                if hasattr(vevent, "location"):
                    ev["location"] = str(vevent.location.value)
                if hasattr(vevent, "description"):
                    ev["description"] = str(vevent.description.value)
                events.append(ev)
    except Exception as e:
        logger.warning(f"vobject parse error: {e}")
    return events


def _format_caldav_event(ev: dict) -> str:
    """Format a CalDAV event dict for display."""
    start = ev.get("start")
    end = ev.get("end")
    title = ev.get("title", "(No title)")
    location = ev.get("location")

    if hasattr(start, "strftime") and hasattr(end, "strftime"):
        try:
            time_str = f"{start.strftime('%I:%M %p')} - {end.strftime('%I:%M %p')}"
        except AttributeError:
            time_str = str(start)
    elif hasattr(start, "strftime"):
        time_str = start.strftime("%I:%M %p")
    else:
        time_str = "All day"

    result = f"• {time_str}: {title}"
    if location:
        result += f"\n  📍 {location}"
    return result


def _caldav_get_events(data: dict, account: dict | None = None) -> str:
    """Get calendar events via CalDAV."""
    try:
        client = _get_caldav_client_for_account(account) if account else _get_caldav_client()
    except RuntimeError as e:
        return f"❌ {e}"

    date_str = data.get("date", "today")
    days = data.get("days", 1)

    now = datetime.now()
    if date_str == "today":
        start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
    elif date_str == "tomorrow":
        start_date = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
    elif date_str == "this week":
        start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
        days = 7
    else:
        try:
            start_date = _parse_datetime(date_str, "00:00")
        except (ValueError, TypeError):
            start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)

    end_date = start_date + timedelta(days=days)

    try:
        principal = client.principal()
        calendars = principal.calendars()
        if not calendars:
            return "📅 No calendars found on this account."

        all_events = []
        for cal in calendars:
            try:
                results = cal.date_search(start=start_date, end=end_date, expand=True)
                for event in results:
                    parsed = _parse_vevent(event.data)
                    all_events.extend(parsed)
            except Exception as e:
                logger.warning(f"Error searching calendar {cal}: {e}")

        if not all_events:
            return f"📅 No events for {date_str}"

        all_events.sort(key=lambda e: str(e.get("start", "")))

        lines = [f"📅 Calendar for {start_date.strftime('%A, %B %d')}:"]
        if days > 1:
            lines[0] = (
                f"📅 Calendar ({start_date.strftime('%b %d')} - {end_date.strftime('%b %d')}):"
            )

        for ev in all_events:
            lines.append(_format_caldav_event(ev))

        return "\n".join(lines)

    except Exception as e:
        logger.error(f"CalDAV error: {e}")
        return f"❌ CalDAV error: {e}"


def _caldav_create_event(data: dict, account: dict | None = None) -> str:
    """Create a calendar event via CalDAV."""
    title = data.get("title", "").strip()
    if not title:
        return "Please provide an event title."

    start_str = data.get("start", "")
    duration_minutes = data.get("duration", 60)

    if not start_str:
        return "Please provide a start date/time."

    if not data.get("_confirmed"):
        from familiar.core.confirmations import calendar_create_preview, needs_confirmation

        attendees = data.get("attendees", [])
        att_display = [
            a if isinstance(a, str) else a.get("email", str(a)) for a in (attendees or [])
        ]
        return needs_confirmation(
            "create_event",
            data,
            calendar_create_preview(title, start_str, duration_minutes, att_display),
            risk="medium",
        )

    try:
        import vobject

        client = _get_caldav_client_for_account(account) if account else _get_caldav_client()
    except (ImportError, RuntimeError) as e:
        return f"❌ {e}"

    try:
        start_dt = _parse_datetime(start_str)
    except ValueError:
        return f"Could not parse start time: {start_str}."

    end_dt = start_dt + timedelta(minutes=duration_minutes)

    cal = vobject.iCalendar()
    vevent = cal.add("vevent")
    vevent.add("summary").value = title
    vevent.add("dtstart").value = start_dt
    vevent.add("dtend").value = end_dt
    location = data.get("location", "")
    if location:
        vevent.add("location").value = location
    description = data.get("description", "")
    if description:
        vevent.add("description").value = description

    try:
        principal = client.principal()
        calendars = principal.calendars()
        if not calendars:
            return "No calendars found."
        calendars[0].save_event(cal.serialize())
        return (
            f"✅ Created: {title}\n"
            f"   📅 {start_dt.strftime('%A, %B %d at %I:%M %p')}\n"
            f"   ⏱️ {duration_minutes} minutes" + (f"\n   📍 {location}" if location else "")
        )
    except Exception as e:
        logger.error(f"CalDAV create error: {e}")
        return f"❌ CalDAV error: {e}"


def _caldav_check_availability(data: dict, account: dict | None = None) -> str:
    """Check if a time slot is available via CalDAV."""
    try:
        client = _get_caldav_client_for_account(account) if account else _get_caldav_client()
    except RuntimeError as e:
        return f"❌ {e}"

    date_str = data.get("date", "today")
    time_str = data.get("time", "")
    duration = data.get("duration", 60)

    try:
        if time_str:
            check_dt = _parse_datetime(f"{date_str} {time_str}")
        else:
            check_dt = _parse_datetime(date_str, "09:00")
    except ValueError:
        return "❌ Could not parse date/time."

    check_end = check_dt + timedelta(minutes=duration)

    try:
        principal = client.principal()
        calendars = principal.calendars()
        conflicts = []
        for cal in calendars:
            try:
                results = cal.date_search(start=check_dt, end=check_end, expand=True)
                for event in results:
                    parsed = _parse_vevent(event.data)
                    for ev in parsed:
                        conflicts.append(ev.get("title", "Busy"))
            except Exception:
                pass

        if not conflicts:
            return f"✅ {check_dt.strftime('%A %I:%M %p')} is available ({duration} min slot)"
        return f"❌ {check_dt.strftime('%A %I:%M %p')} has conflicts:\n" + "\n".join(
            f"  • {c}" for c in conflicts
        )
    except Exception as e:
        return f"❌ CalDAV error: {e}"


def _caldav_find_free_time(data: dict, account: dict | None = None) -> str:
    """Find available time slots on a given day via CalDAV."""
    try:
        client = _get_caldav_client_for_account(account) if account else _get_caldav_client()
    except RuntimeError as e:
        return f"❌ {e}"

    date_str = data.get("date", "today")
    duration = data.get("duration", 60)
    start_hour = data.get("start_hour", 9)
    end_hour = data.get("end_hour", 17)

    now = datetime.now()
    if date_str == "today":
        day = now.date()
    elif date_str == "tomorrow":
        day = (now + timedelta(days=1)).date()
    else:
        try:
            day = _parse_datetime(date_str, "00:00").date()
        except (ValueError, TypeError):
            day = now.date()

    day_start = datetime.combine(day, datetime.min.time().replace(hour=start_hour))
    day_end = datetime.combine(day, datetime.min.time().replace(hour=end_hour))

    try:
        principal = client.principal()
        calendars = principal.calendars()
        busy = []
        for cal in calendars:
            try:
                results = cal.date_search(start=day_start, end=day_end, expand=True)
                for event in results:
                    parsed = _parse_vevent(event.data)
                    for ev in parsed:
                        s = ev.get("start")
                        e = ev.get("end")
                        if hasattr(s, "hour") and hasattr(e, "hour"):
                            if not hasattr(s, "date"):
                                s = datetime.combine(day, s)
                            if not hasattr(e, "date"):
                                e = datetime.combine(day, e)
                            busy.append((s, e))
            except Exception:
                pass

        busy.sort()
        free_slots = []
        current = day_start
        for busy_start, busy_end in busy:
            if current + timedelta(minutes=duration) <= busy_start:
                free_slots.append((current, busy_start))
            current = max(current, busy_end)
        if current + timedelta(minutes=duration) <= day_end:
            free_slots.append((current, day_end))

        if not free_slots:
            return f"❌ No {duration}-minute slots available on {day.strftime('%A, %B %d')}"

        lines = [f"✅ Available times on {day.strftime('%A, %B %d')}:"]
        for slot_start, slot_end in free_slots[:5]:
            lines.append(f"  • {slot_start.strftime('%I:%M %p')} - {slot_end.strftime('%I:%M %p')}")
        return "\n".join(lines)

    except Exception as e:
        return f"❌ CalDAV error: {e}"


def _caldav_delete_event(data: dict, account: dict | None = None) -> str:
    """Delete a calendar event by title/date via CalDAV."""
    search = data.get("search", "").strip()
    if not search:
        return "Please provide an event title to search for."

    date_str = data.get("date", "today")
    now = datetime.now()
    if date_str == "today":
        day = now.date()
    elif date_str == "tomorrow":
        day = (now + timedelta(days=1)).date()
    else:
        try:
            day = _parse_datetime(date_str, "00:00").date()
        except (ValueError, TypeError):
            day = now.date()

    day_start = datetime.combine(day, datetime.min.time())
    day_end = day_start + timedelta(days=1)

    try:
        client = _get_caldav_client_for_account(account) if account else _get_caldav_client()
    except RuntimeError as e:
        return f"❌ {e}"

    if not data.get("_confirmed"):
        from familiar.core.confirmations import calendar_delete_preview, needs_confirmation

        # Find the event first for preview
        try:
            principal = client.principal()
            calendars = principal.calendars()
            for cal in calendars:
                results = cal.date_search(start=day_start, end=day_end, expand=True)
                for event in results:
                    parsed = _parse_vevent(event.data)
                    for ev in parsed:
                        if search.lower() in ev.get("title", "").lower():
                            return needs_confirmation(
                                "delete_event",
                                data,
                                calendar_delete_preview(
                                    ev.get("title", search),
                                    str(ev.get("start", "")),
                                ),
                                risk="high",
                            )
        except Exception:
            pass
        return needs_confirmation(
            "delete_event",
            data,
            calendar_delete_preview(search, date_str),
            risk="high",
        )

    try:
        principal = client.principal()
        calendars = principal.calendars()
        for cal in calendars:
            try:
                results = cal.date_search(start=day_start, end=day_end, expand=True)
                for event in results:
                    parsed = _parse_vevent(event.data)
                    for ev in parsed:
                        if search.lower() in ev.get("title", "").lower():
                            event.delete()
                            return f"✅ Deleted: {ev.get('title', search)}"
            except Exception:
                pass

        return f"No events matching '{search}' on {day.strftime('%B %d')}."

    except Exception as e:
        logger.error(f"CalDAV delete error: {e}")
        return f"❌ CalDAV error: {e}"


# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------


def _parse_datetime(dt_str: str, default_time: str = "09:00") -> datetime:
    """Parse various datetime formats."""
    dt_str = dt_str.lower().strip()
    now = datetime.now()
    today = now.date()

    # Relative dates
    if dt_str == "today":
        return datetime.combine(today, datetime.strptime(default_time, "%H:%M").time())
    elif dt_str == "tomorrow":
        return datetime.combine(
            today + timedelta(days=1), datetime.strptime(default_time, "%H:%M").time()
        )
    elif dt_str.startswith("next "):
        day_name = dt_str[5:]
        days = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
        if day_name in days:
            target_day = days.index(day_name)
            current_day = today.weekday()
            days_ahead = target_day - current_day
            if days_ahead <= 0:
                days_ahead += 7
            return datetime.combine(
                today + timedelta(days=days_ahead), datetime.strptime(default_time, "%H:%M").time()
            )

    # Try various formats
    formats = [
        "%Y-%m-%d %H:%M",
        "%Y-%m-%d",
        "%m/%d/%Y %H:%M",
        "%m/%d/%Y",
        "%m/%d %H:%M",
        "%m/%d",
        "%B %d %H:%M",
        "%B %d",
        "%b %d %H:%M",
        "%b %d",
    ]

    for fmt in formats:
        try:
            parsed = datetime.strptime(dt_str, fmt)
            if parsed.year == 1900:
                parsed = parsed.replace(year=today.year)
            if parsed.hour == 0 and parsed.minute == 0 and ":" not in dt_str:
                parsed = parsed.replace(hour=int(default_time.split(":")[0]))
            return parsed
        except ValueError:
            continue

    raise ValueError(f"Could not parse datetime: {dt_str}")


def _format_event(event: dict) -> str:
    """Format a Google Calendar event for display."""
    start = event.get("start", {})
    end = event.get("end", {})

    # Handle all-day vs timed events
    if "dateTime" in start:
        start_dt = datetime.fromisoformat(start["dateTime"].replace("Z", "+00:00"))
        end_dt = datetime.fromisoformat(end["dateTime"].replace("Z", "+00:00"))
        time_str = f"{start_dt.strftime('%I:%M %p')} - {end_dt.strftime('%I:%M %p')}"
    else:
        time_str = "All day"

    summary = event.get("summary", "(No title)")
    location = event.get("location", "")

    result = f"• {time_str}: {summary}"
    if location:
        result += f"\n  📍 {location}"

    return result


# ---------------------------------------------------------------------------
# Google Calendar impl functions (extracted for account-aware dispatch)
# ---------------------------------------------------------------------------


def _google_get_events_impl(data: dict, service, calendar_id: str) -> str:
    """Get events from a specific Google Calendar."""
    date_str = data.get("date", "today")
    days = data.get("days", 1)

    try:
        if date_str == "today":
            start_date = datetime.now(timezone.utc).replace(
                hour=0, minute=0, second=0, microsecond=0
            )
        elif date_str == "tomorrow":
            start_date = (datetime.now(timezone.utc) + timedelta(days=1)).replace(
                hour=0, minute=0, second=0, microsecond=0
            )
        elif date_str == "this week":
            start_date = datetime.now(timezone.utc).replace(
                hour=0, minute=0, second=0, microsecond=0
            )
            days = 7
        else:
            start_date = _parse_datetime(date_str, "00:00").replace(tzinfo=timezone.utc)
    except (ValueError, TypeError):
        start_date = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)

    end_date = start_date + timedelta(days=days)

    try:
        events_result = (
            service.events()
            .list(
                calendarId=calendar_id,
                timeMin=start_date.isoformat(),
                timeMax=end_date.isoformat(),
                singleEvents=True,
                orderBy="startTime",
                maxResults=20,
            )
            .execute()
        )

        events = events_result.get("items", [])

        if not events:
            return f"📅 No events for {date_str}"

        lines = [f"📅 Calendar for {start_date.strftime('%A, %B %d')}:"]
        if days > 1:
            lines[0] = (
                f"📅 Calendar ({start_date.strftime('%b %d')} - {end_date.strftime('%b %d')}):"
            )

        current_date = None
        for event in events:
            start = event.get("start", {})
            if "dateTime" in start:
                event_date = datetime.fromisoformat(start["dateTime"].replace("Z", "+00:00")).date()
            else:
                event_date = datetime.fromisoformat(start["date"]).date()

            if days > 1 and event_date != current_date:
                current_date = event_date
                lines.append(f"\n{event_date.strftime('%A, %b %d')}:")

            lines.append(_format_event(event))

        return "\n".join(lines)

    except Exception as e:
        logger.error(f"Calendar operation failed: {e}")
        return "❌ Calendar operation failed."


def _google_create_event_impl(data: dict, service, calendar_id: str) -> str:
    """Create an event on a specific Google Calendar."""
    title = data.get("title", "").strip()
    if not title:
        return "Please provide an event title"

    start_str = data.get("start", "")
    duration_minutes = data.get("duration", 60)
    location = data.get("location", "")
    description = data.get("description", "")
    attendees = data.get("attendees", [])

    try:
        start_dt = _parse_datetime(start_str)
    except ValueError as e:
        logger.error(f"Calendar operation failed: {e}")
        return "❌ Calendar operation failed. Check OAuth credentials."

    if not data.get("_confirmed"):
        from familiar.core.confirmations import calendar_create_preview, needs_confirmation

        att_display = [
            a if isinstance(a, str) else a.get("email", str(a)) for a in (attendees or [])
        ]
        return needs_confirmation(
            "create_event",
            data,
            calendar_create_preview(title, start_str, duration_minutes, att_display),
            risk="medium",
        )

    end_dt = start_dt + timedelta(minutes=duration_minutes)

    # Timezone: CALENDAR_TIMEZONE env var, then config, then UTC
    _tz = os.environ.get("CALENDAR_TIMEZONE", "")
    if not _tz:
        try:
            from familiar.core.config import load_config

            _cfg = load_config()
            _tz = (
                getattr(_cfg, "timezone", "")
                or getattr(getattr(_cfg, "agent", None), "timezone", "")
                or ""
            )
        except Exception:
            pass
    if not _tz:
        _tz = "UTC"

    event = {
        "summary": title,
        "start": {
            "dateTime": start_dt.isoformat(),
            "timeZone": _tz,
        },
        "end": {
            "dateTime": end_dt.isoformat(),
            "timeZone": _tz,
        },
    }

    if location:
        event["location"] = location
    if description:
        event["description"] = description
    if attendees:
        event["attendees"] = [{"email": email} for email in attendees]

    try:
        created = service.events().insert(calendarId=calendar_id, body=event).execute()
        event_link = created.get("htmlLink", "")

        return (
            f"✅ Created: {title}\n"
            f"   📅 {start_dt.strftime('%A, %B %d at %I:%M %p')}\n"
            f"   ⏱️ {duration_minutes} minutes"
            + (f"\n   📍 {location}" if location else "")
            + (f"\n   🔗 {event_link}" if event_link else "")
        )
    except Exception as e:
        logger.error(f"Calendar operation failed: {e}")
        return "❌ Calendar operation failed."


def _google_check_availability_impl(data: dict, service, calendar_id: str) -> str:
    """Check availability on a specific Google Calendar."""
    date_str = data.get("date", "today")
    time_str = data.get("time", "")
    duration = data.get("duration", 60)

    try:
        if time_str:
            check_dt = _parse_datetime(f"{date_str} {time_str}")
        else:
            check_dt = _parse_datetime(date_str, "09:00")
    except ValueError as e:
        logger.error(f"Calendar operation failed: {e}")
        return "❌ Calendar operation failed. Check OAuth credentials."

    check_end = check_dt + timedelta(minutes=duration)

    try:
        events_result = (
            service.events()
            .list(
                calendarId=calendar_id,
                timeMin=check_dt.isoformat() + "Z",
                timeMax=check_end.isoformat() + "Z",
                singleEvents=True,
                orderBy="startTime",
            )
            .execute()
        )

        events = events_result.get("items", [])

        if not events:
            return f"✅ {check_dt.strftime('%A %I:%M %p')} is available ({duration} min slot)"
        else:
            conflicts = [e.get("summary", "Busy") for e in events]
            return f"❌ {check_dt.strftime('%A %I:%M %p')} has conflicts:\n" + "\n".join(
                f"  • {c}" for c in conflicts
            )
    except Exception as e:
        return f"❌ Error: {e}"


def _google_find_free_time_impl(data: dict, service, calendar_id: str) -> str:
    """Find free time on a specific Google Calendar."""
    date_str = data.get("date", "today")
    duration = data.get("duration", 60)
    start_hour = data.get("start_hour", 9)
    end_hour = data.get("end_hour", 17)

    try:
        day = _parse_datetime(date_str, "00:00").date()
    except (ValueError, TypeError):
        day = datetime.now().date()

    day_start = datetime.combine(day, datetime.min.time().replace(hour=start_hour))
    day_end = datetime.combine(day, datetime.min.time().replace(hour=end_hour))

    try:
        events_result = (
            service.events()
            .list(
                calendarId=calendar_id,
                timeMin=day_start.isoformat() + "Z",
                timeMax=day_end.isoformat() + "Z",
                singleEvents=True,
                orderBy="startTime",
            )
            .execute()
        )

        events = events_result.get("items", [])

        # Build list of busy times
        busy = []
        for event in events:
            start = event.get("start", {})
            end = event.get("end", {})
            if "dateTime" in start:
                busy.append(
                    (
                        datetime.fromisoformat(start["dateTime"].replace("Z", "+00:00")).replace(
                            tzinfo=None
                        ),
                        datetime.fromisoformat(end["dateTime"].replace("Z", "+00:00")).replace(
                            tzinfo=None
                        ),
                    )
                )

        # Find free slots
        free_slots = []
        current = day_start

        for busy_start, busy_end in sorted(busy):
            if current + timedelta(minutes=duration) <= busy_start:
                free_slots.append((current, busy_start))
            current = max(current, busy_end)

        if current + timedelta(minutes=duration) <= day_end:
            free_slots.append((current, day_end))

        if not free_slots:
            return f"❌ No {duration}-minute slots available on {day.strftime('%A, %B %d')}"

        lines = [f"✅ Available times on {day.strftime('%A, %B %d')}:"]
        for slot_start, slot_end in free_slots[:5]:
            lines.append(f"  • {slot_start.strftime('%I:%M %p')} - {slot_end.strftime('%I:%M %p')}")

        return "\n".join(lines)

    except Exception as e:
        return f"❌ Error: {e}"


def _google_delete_event_impl(data: dict, service, calendar_id: str) -> str:
    """Delete an event from a specific Google Calendar."""
    search = data.get("search", "")
    date_str = data.get("date", "today")

    if not search:
        return "Please provide event title to search for"

    try:
        day = _parse_datetime(date_str, "00:00")
    except (ValueError, TypeError):
        day = datetime.now()

    day_start = day.replace(hour=0, minute=0, second=0)
    day_end = day_start + timedelta(days=1)

    try:
        events_result = (
            service.events()
            .list(
                calendarId=calendar_id,
                timeMin=day_start.isoformat() + "Z",
                timeMax=day_end.isoformat() + "Z",
                singleEvents=True,
                orderBy="startTime",
                q=search,
            )
            .execute()
        )

        events = events_result.get("items", [])

        if not events:
            return f"No events found matching '{search}' on {day.strftime('%B %d')}"

        if len(events) > 1 and not data.get("_confirmed"):
            lines = [f"Found {len(events)} matching events:"]
            for e in events:
                lines.append(
                    f"  • {e.get('summary')} — {e.get('start', {}).get('dateTime', '')[:16]}"
                )
            lines.append("\nPlease be more specific so I can confirm the right one.")
            return "\n".join(lines)

        event = events[0]
        event_title = event.get("summary", search)
        event_start = event.get("start", {}).get("dateTime", "")[:16]

        if not data.get("_confirmed"):
            from familiar.core.confirmations import calendar_delete_preview, needs_confirmation

            confirmed_input = dict(data)
            confirmed_input["_event_id"] = event["id"]
            return needs_confirmation(
                "delete_event",
                confirmed_input,
                calendar_delete_preview(event_title, event_start),
                risk="high",
            )

        event_id = data.get("_event_id") or event["id"]
        service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
        return f"✅ Deleted: {event_title}"

    except Exception as e:
        return f"❌ Error: {e}"


# ---------------------------------------------------------------------------
# Account-aware dispatch helpers
# ---------------------------------------------------------------------------


def _get_events_for_account(data: dict, account: dict) -> str:
    """Route get_events to the right backend for one account."""
    label = account.get("label") or account["id"]
    if account["type"] == "caldav":
        return _caldav_get_events(data, account=account)
    elif account["type"] == "google":
        service, cal_id = _get_google_service_for_account(account)
        return _google_get_events_impl(data, service, cal_id)
    return f"❌ {label}: unknown account type '{account['type']}'"


def _create_event_for_account(data: dict, account: dict) -> str:
    """Route create_event to the right backend for one account."""
    label = account.get("label") or account["id"]
    if account["type"] == "caldav":
        return _caldav_create_event(data, account=account)
    elif account["type"] == "google":
        service, cal_id = _get_google_service_for_account(account)
        return _google_create_event_impl(data, service, cal_id)
    return f"❌ {label}: unknown account type '{account['type']}'"


def _check_availability_for_account(data: dict, account: dict) -> str:
    """Route check_availability to the right backend for one account."""
    label = account.get("label") or account["id"]
    if account["type"] == "caldav":
        return _caldav_check_availability(data, account=account)
    elif account["type"] == "google":
        service, cal_id = _get_google_service_for_account(account)
        return _google_check_availability_impl(data, service, cal_id)
    return f"❌ {label}: unknown account type '{account['type']}'"


def _find_free_time_for_account(data: dict, account: dict) -> str:
    """Route find_free_time to the right backend for one account."""
    label = account.get("label") or account["id"]
    if account["type"] == "caldav":
        return _caldav_find_free_time(data, account=account)
    elif account["type"] == "google":
        service, cal_id = _get_google_service_for_account(account)
        return _google_find_free_time_impl(data, service, cal_id)
    return f"❌ {label}: unknown account type '{account['type']}'"


def _delete_event_for_account(data: dict, account: dict) -> str:
    """Route delete_event to the right backend for one account."""
    label = account.get("label") or account["id"]
    if account["type"] == "caldav":
        return _caldav_delete_event(data, account=account)
    elif account["type"] == "google":
        service, cal_id = _get_google_service_for_account(account)
        return _google_delete_event_impl(data, service, cal_id)
    return f"❌ {label}: unknown account type '{account['type']}'"


# ---------------------------------------------------------------------------
# Multi-account menu helpers
# ---------------------------------------------------------------------------


def _calendar_extract_summary(result: str) -> str:
    """Parse calendar result to a short summary like '3 events'."""
    if "No events" in result or "No calendars" in result:
        return "0 events"
    if result.startswith("❌"):
        return "error"
    count = result.count("\n• ")
    if count:
        return f"{count} event{'s' if count != 1 else ''}"
    return "checked"


def _fan_out_calendar(
    data: dict,
    accounts: list[dict],
    executor,
    menu_kind: str,
) -> str:
    """Shared multi-account fan-out. Returns plain text or sentinel for menu."""
    from familiar.core.confirmations import CALENDAR_MENU_PREFIX

    per_account = {}
    summaries = []

    for acct in accounts:
        label = acct.get("label") or acct["id"]
        try:
            result = executor(data, acct)
            per_account[acct["id"]] = result
            summaries.append((acct["id"], label, _calendar_extract_summary(result)))
        except Exception as e:
            err = f"❌ {label}: {e}"
            per_account[acct["id"]] = err
            summaries.append((acct["id"], label, "error"))

    if not per_account:
        return "❌ No calendar accounts configured. Run /connect calendar to set up."

    if len(per_account) == 1:
        return next(iter(per_account.values()))

    ctx = data.get("_context") or {}
    session = ctx.get("session")
    if session is None:
        parts = []
        for acct_id, text in per_account.items():
            label = next((s[1] for s in summaries if s[0] == acct_id), acct_id)
            parts.append(f"📌 {label}:\n{text}")
        return "\n\n".join(parts)

    import secrets as _sec

    token = _sec.token_hex(8)
    session.session_context["calendar_menu"] = {
        "token": token,
        "kind": menu_kind,
        "results": per_account,
        "summaries": summaries,
    }
    session_mgr = ctx.get("session_manager")
    if session_mgr:
        session_mgr.save_session(session)

    return f"{CALENDAR_MENU_PREFIX}{token}"


# ---------------------------------------------------------------------------
# Public handlers — multi-account dispatch
# ---------------------------------------------------------------------------


def get_events(data: dict) -> str:
    """Get calendar events. Fans out across all accounts unless one is specified."""
    from .accounts import get_account, get_all_accounts

    account_id = data.get("account")
    if account_id is not None:
        acct = get_account(account_id)
        if not acct:
            return f"No calendar account matching '{account_id}'."
        return _get_events_for_account(data, acct)

    accounts = get_all_accounts()
    if not accounts:
        # Legacy fallback
        provider = _calendar_provider()
        if provider == "none":
            return _NOT_CONFIGURED_MSG
        if provider == "caldav":
            return _caldav_get_events(data)
        # Google legacy
        try:
            service = _get_calendar_service()
        except Exception as e:
            logger.error(f"Calendar operation failed: {e}")
            return "❌ Calendar operation failed. Check OAuth credentials."
        return _google_get_events_impl(data, service, "primary")

    if len(accounts) == 1:
        return _get_events_for_account(data, accounts[0])

    return _fan_out_calendar(data, accounts, _get_events_for_account, "events")


def create_event(data: dict) -> str:
    """Create a calendar event on the primary or specified account."""
    from .accounts import get_account

    account_id = data.get("account")
    acct = get_account(account_id)
    if acct:
        return _create_event_for_account(data, acct)

    if account_id is not None:
        return f"No calendar account matching '{account_id}'."

    # Legacy fallback (no accounts in registry)
    provider = _calendar_provider()
    if provider == "caldav":
        return _caldav_create_event(data)
    if provider == "none":
        return _NOT_CONFIGURED_MSG
    try:
        service = _get_calendar_service()
    except Exception as e:
        logger.error(f"Calendar operation failed: {e}")
        return "❌ Calendar operation failed. Check OAuth credentials."
    return _google_create_event_impl(data, service, "primary")


def check_availability(data: dict) -> str:
    """Check if a time slot is available. Fans out across all accounts unless one is specified."""
    from .accounts import get_account, get_all_accounts

    account_id = data.get("account")
    if account_id is not None:
        acct = get_account(account_id)
        if not acct:
            return f"No calendar account matching '{account_id}'."
        return _check_availability_for_account(data, acct)

    accounts = get_all_accounts()
    if not accounts:
        provider = _calendar_provider()
        if provider == "none":
            return _NOT_CONFIGURED_MSG
        if provider == "caldav":
            return _caldav_check_availability(data)
        try:
            service = _get_calendar_service()
        except Exception as e:
            logger.error(f"Calendar operation failed: {e}")
            return "❌ Calendar operation failed. Check OAuth credentials."
        return _google_check_availability_impl(data, service, "primary")

    if len(accounts) == 1:
        return _check_availability_for_account(data, accounts[0])

    return _fan_out_calendar(data, accounts, _check_availability_for_account, "availability")


def find_free_time(data: dict) -> str:
    """Find available time slots. Fans out across all accounts unless one is specified."""
    from .accounts import get_account, get_all_accounts

    account_id = data.get("account")
    if account_id is not None:
        acct = get_account(account_id)
        if not acct:
            return f"No calendar account matching '{account_id}'."
        return _find_free_time_for_account(data, acct)

    accounts = get_all_accounts()
    if not accounts:
        provider = _calendar_provider()
        if provider == "none":
            return _NOT_CONFIGURED_MSG
        if provider == "caldav":
            return _caldav_find_free_time(data)
        try:
            service = _get_calendar_service()
        except Exception as e:
            logger.error(f"Calendar operation failed: {e}")
            return "❌ Calendar operation failed. Check OAuth credentials."
        return _google_find_free_time_impl(data, service, "primary")

    if len(accounts) == 1:
        return _find_free_time_for_account(data, accounts[0])

    return _fan_out_calendar(data, accounts, _find_free_time_for_account, "free_time")


def delete_event(data: dict) -> str:
    """Delete/cancel an event on the primary or specified account."""
    from .accounts import get_account

    account_id = data.get("account")
    acct = get_account(account_id)
    if acct:
        return _delete_event_for_account(data, acct)

    if account_id is not None:
        return f"No calendar account matching '{account_id}'."

    # Legacy fallback
    provider = _calendar_provider()
    if provider == "caldav":
        return _caldav_delete_event(data)
    if provider == "none":
        return _NOT_CONFIGURED_MSG
    try:
        service = _get_calendar_service()
    except Exception as e:
        logger.error(f"Calendar operation failed: {e}")
        return "I can't access your calendar yet. Run /connect google auth calendar to connect it."
    return _google_delete_event_impl(data, service, "primary")


# Tool definitions
_ACCOUNT_FIELD_READ = {
    "type": "string",
    "description": "Calendar account: 'google', 'caldav', 'work', etc. Omit to check all accounts.",
}
_ACCOUNT_FIELD_WRITE = {
    "type": "string",
    "description": "Calendar account: 'google', 'caldav', 'work', etc. Omit for primary account.",
}

TOOLS = [
    {
        "name": "calendar_today",
        "description": (
            "Get today's calendar events, or events for a specific date/range. "
            "Use this when the user asks what's on their schedule, what meetings they have, "
            "or to review upcoming events. Supports relative dates (today, tomorrow, this week) "
            "and absolute dates. Returns a formatted list of events with times and locations."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "date": {
                    "type": "string",
                    "description": "Date to check (today, tomorrow, this week, next Monday, 2024-02-15)",
                    "default": "today",
                },
                "days": {"type": "integer", "description": "Number of days to show", "default": 1},
                "account": _ACCOUNT_FIELD_READ,
            },
        },
        "handler": get_events,
        "category": "calendar",
    },
    {
        "name": "calendar_create",
        "description": (
            "Create a new calendar event with title, start time, duration, and optional "
            "location or attendees. Use this when the user wants to schedule a meeting, "
            "appointment, or reminder. Requires confirmation before the event is created. "
            "Returns the created event details including a link if available."
        ),
        "requires_confirmation": True,
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {"type": "string", "description": "Event title"},
                "start": {
                    "type": "string",
                    "description": "Start date/time (tomorrow 2pm, next Tuesday 10:00, 2024-02-15 14:30)",
                },
                "duration": {
                    "type": "integer",
                    "description": "Duration in minutes",
                    "default": 60,
                },
                "location": {"type": "string", "description": "Event location or meeting link"},
                "description": {"type": "string", "description": "Event description or notes"},
                "attendees": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of attendee email addresses",
                },
                "account": _ACCOUNT_FIELD_WRITE,
            },
            "required": ["title", "start"],
        },
        "handler": create_event,
        "category": "calendar",
    },
    {
        "name": "calendar_check",
        "description": (
            "Check if a specific time slot is available on the calendar. "
            "Use this when the user asks whether a particular date and time is free "
            "before scheduling. Returns availability status and lists any conflicting "
            "events if the slot is busy."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "date": {"type": "string", "description": "Date to check"},
                "time": {"type": "string", "description": "Time to check (2pm, 14:30)"},
                "duration": {
                    "type": "integer",
                    "description": "Duration needed in minutes",
                    "default": 60,
                },
                "account": _ACCOUNT_FIELD_READ,
            },
            "required": ["date"],
        },
        "handler": check_availability,
        "category": "calendar",
    },
    {
        "name": "calendar_free",
        "description": (
            "Find available time slots on a given day within business hours. "
            "Use this when the user needs to find open windows for scheduling, "
            "such as 'when am I free tomorrow?' Configurable start/end hours and "
            "minimum duration. Returns up to five available slots."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "date": {
                    "type": "string",
                    "description": "Date to check for free slots (today, tomorrow, 2024-02-15)",
                    "default": "today",
                },
                "duration": {"type": "integer", "description": "Minutes needed", "default": 60},
                "start_hour": {
                    "type": "integer",
                    "description": "Earliest hour (24h)",
                    "default": 9,
                },
                "end_hour": {"type": "integer", "description": "Latest hour (24h)", "default": 17},
                "account": _ACCOUNT_FIELD_READ,
            },
        },
        "handler": find_free_time,
        "category": "calendar",
    },
    {
        "name": "calendar_delete",
        "description": (
            "Delete or cancel a calendar event by searching for its title on a given date. "
            "Use this when the user wants to remove or cancel a scheduled event. "
            "Requires confirmation before deletion. If multiple events match the search "
            "term, the user will be prompted to narrow their query."
        ),
        "requires_confirmation": True,
        "input_schema": {
            "type": "object",
            "properties": {
                "search": {"type": "string", "description": "Event title to search for"},
                "date": {"type": "string", "default": "today"},
                "confirm": {"type": "boolean", "default": False},
                "account": _ACCOUNT_FIELD_WRITE,
            },
            "required": ["search"],
        },
        "handler": delete_event,
        "category": "calendar",
    },
]
