import html
import os
import re
import uuid
from datetime import datetime, timedelta, timezone
from urllib.parse import parse_qs, quote, unquote, urlparse

import requests
from sqlalchemy.orm import Session

from app import crud
from app.logging_utils import get_logger
from app.models import RecurringMeetingCandidate, User
from app.services import recall_service

GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_EVENTS_URL = "https://www.googleapis.com/calendar/v3/calendars/{calendar_id}/events"
SUPPORTED_PLATFORMS = {"google_meet", "zoom", "microsoft_teams"}
DEFAULT_SYNC_DAYS = int(os.getenv("GOOGLE_CALENDAR_SYNC_DAYS", "30"))
IMMEDIATE_JOIN_GRACE_MINUTES = int(os.getenv("GOOGLE_CALENDAR_IMMEDIATE_JOIN_GRACE_MINUTES", "15"))
RECURRING_MEETING_WINDOW_DAYS = int(os.getenv("RECURRING_MEETING_WINDOW_DAYS", "5"))
logger = get_logger(__name__)


def _is_future_calendar_event(start_time: datetime, now: datetime | None = None) -> bool:
    reference_time = now or datetime.now(timezone.utc)
    return start_time > reference_time


def _extract_google_error_payload(response: requests.Response) -> tuple[str | None, str]:
    try:
        payload = response.json()
    except ValueError:
        return None, (response.text or "").strip()

    if not isinstance(payload, dict):
        return None, str(payload)

    error_code = payload.get("error")
    error_description = payload.get("error_description")
    if error_description:
        return error_code, str(error_description)
    return error_code, str(payload)


def refresh_google_token(db, user: User) -> str:
    if not user.refresh_token:
        raise RuntimeError("Google refresh token is missing for this user")

    client_id = os.getenv("GOOGLE_CLIENT_ID")
    client_secret = os.getenv("GOOGLE_CLIENT_SECRET")
    if not client_id or not client_secret:
        raise RuntimeError("Google OAuth client settings are missing")

    response = requests.post(
        GOOGLE_TOKEN_URL,
        data={
            "client_id": client_id,
            "client_secret": client_secret,
            "refresh_token": user.refresh_token,
            "grant_type": "refresh_token",
        },
        timeout=60,
    )
    if not response.ok:
        error_code, error_detail = _extract_google_error_payload(response)
        logger.warning(
            "Google token refresh failed for user %s. Error code: %s. Detail: %s.",
            user.id,
            error_code,
            error_detail,
        )
        if error_code == "invalid_grant":
            raise RuntimeError(
                f"Google refresh token is no longer valid for user {user.id}. Re-login is required."
            )
        if error_code == "invalid_client":
            raise RuntimeError(
                "Google OAuth client credentials are invalid. Check GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET."
            )
        raise RuntimeError(f"Google token refresh failed: {error_detail}")

    token_data = response.json()
    user.access_token = token_data["access_token"]
    db.commit()
    db.refresh(user)
    return user.access_token


def call_google_api(db, user: User, method: str, url: str, **kwargs):
    headers = dict(kwargs.pop("headers", {}))
    headers["Authorization"] = f"Bearer {user.access_token}"
    response = requests.request(method, url, headers=headers, timeout=60, **kwargs)

    if response.status_code != 401:
        try:
            response.raise_for_status()
        except requests.exceptions.HTTPError as exc:
            logger.error("Google API Error [%s]: %s", response.status_code, response.text)
            raise
        return response

    new_token = refresh_google_token(db, user)
    headers["Authorization"] = f"Bearer {new_token}"
    response = requests.request(method, url, headers=headers, timeout=60, **kwargs)
    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError as exc:
        logger.error("Google API Error [%s]: %s", response.status_code, response.text)
        raise
    return response

#
# def start_google_watch(db, user: User, webhook_url: str, days_ahead: int = DEFAULT_SYNC_DAYS, auto_schedule_bot: bool = True):
#     if not user.calendar_sync_enabled:
#         raise RuntimeError("Calendar sync is disabled for this user")
#     validate_google_webhook_url(webhook_url)
#     print(
#         f"[google-watch] start user_id={user.id} webhook_url={webhook_url} "
#         f"days_ahead={days_ahead} auto_schedule_bot={auto_schedule_bot}"
#     )
#     sync_result = sync_google_calendar(
#         db=db,
#         user=user,
#         days_ahead=days_ahead,
#         auto_schedule_bot=auto_schedule_bot,
#         full_sync=True,
#     )
#     watch_state = create_google_watch(db, user, webhook_url)
#     sync_token = create_google_sync_token(db, user)
#     if sync_token:
#         watch_state = crud.save_calendar_sync_state(
#             db=db,
#             user_id=user.id,
#             calendar_id=watch_state.calendar_id,
#             sync_token=sync_token,
#         )
#     print(
#         f"[google-watch] created user_id={user.id} channel_id={watch_state.channel_id} "
#         f"sync_token={watch_state.sync_token} expires_at={watch_state.expires_at}"
#     )
#     return {"sync_result": sync_result, "watch_state": watch_state}
#
#
# def ensure_google_watch(db, user: User, webhook_url: str, days_ahead: int = DEFAULT_SYNC_DAYS, auto_schedule_bot: bool = True):
#     if not user.calendar_sync_enabled:
#         return {"sync_result": {"synced": [], "skipped": [], "deleted": [], "sync_token": None}, "watch_state": None}
#
#     state = crud.get_calendar_sync_state(db, user.id)
#     now = datetime.now(timezone.utc)
#     if (
#         state
#         and state.channel_id
#         and state.expires_at
#         and state.expires_at > now + timedelta(hours=12)
#         and state.webhook_url == webhook_url
#     ):
#         print(
#             f"[google-watch] keep existing watch user_id={user.id} "
#             f"channel_id={state.channel_id} expires_at={state.expires_at}"
#         )
#         return {"sync_result": {"synced": [], "skipped": [], "deleted": [], "sync_token": state.sync_token}, "watch_state": state}
#
#     if state and state.webhook_url and state.webhook_url != webhook_url:
#         print(
#             f"[google-watch] recreating watch user_id={user.id} "
#             f"reason=webhook-url-changed old={state.webhook_url} new={webhook_url}"
#         )
#
#     return start_google_watch(
#         db=db,
#         user=user,
#         webhook_url=webhook_url,
#         days_ahead=days_ahead,
#         auto_schedule_bot=auto_schedule_bot,
#     )

def ensure_google_watch(
    db,
    user: User,
    webhook_url: str,
    days_ahead: int = DEFAULT_SYNC_DAYS,
    auto_schedule_bot: bool = True,
):
    # ✅ REMOVED: `if not user.calendar_sync_enabled: return`
    # Reason: caller (auth/callback) already verified scope.
    # This guard was causing first-login to silently skip watch setup.
    # calendar_sync_enabled is now set to True before this is called.
    #
    # KEEP this safety check only for explicit opt-out (disabled by user):
    if user.calendar_sync_enabled is False:
        # Only skip if user has explicitly disabled — not if it's a new user
        logger.info("Google Calendar watch was skipped for user %s because calendar sync is disabled.", user.id)
        return {
            "sync_result": {"synced": [], "skipped": [], "deleted": [], "sync_token": None},
            "watch_state": None,
        }

    state = crud.get_calendar_sync_state(db, user.id)
    now   = datetime.now(timezone.utc)

    if (
        state
        and state.channel_id
        and state.expires_at
        and state.expires_at > now + timedelta(hours=12)
        and state.webhook_url == webhook_url
    ):
        logger.info(
            "The existing Google Calendar watch is still valid for user %s. Channel id: %s. Expires at: %s.",
            user.id,
            state.channel_id,
            state.expires_at,
        )
        return {
            "sync_result": {"synced": [], "skipped": [], "deleted": [], "sync_token": state.sync_token},
            "watch_state": state,
        }

    if state and state.webhook_url and state.webhook_url != webhook_url:
        logger.info(
            "The Google Calendar watch will be recreated for user %s because the webhook URL changed from %s to %s.",
            user.id,
            state.webhook_url,
            webhook_url,
        )

    return start_google_watch(
        db=db,
        user=user,
        webhook_url=webhook_url,
        days_ahead=days_ahead,
        auto_schedule_bot=auto_schedule_bot,
    )


def start_google_watch(
    db,
    user: User,
    webhook_url: str,
    days_ahead: int = DEFAULT_SYNC_DAYS,
    auto_schedule_bot: bool = True,
):
    # ✅ REMOVED the hard raise — replaced with a clear log + early return
    # Reason: same as above — by the time we reach here, the caller has
    # already guaranteed calendar_sync_enabled=True.
    # Keeping a raise here was masking the real bug as a silent swallowed exception.
    if not user.calendar_sync_enabled:
        logger.warning("Google Calendar watch start was requested for user %s, but calendar sync is disabled.", user.id)
        return {
            "sync_result": {"synced": [], "skipped": [], "deleted": [], "sync_token": None},
            "watch_state": None,
        }

    validate_google_webhook_url(webhook_url)
    logger.info(
        "Google Calendar watch setup has started for user %s. Webhook URL: %s. Days ahead: %s. Auto schedule bot: %s.",
        user.id,
        webhook_url,
        days_ahead,
        auto_schedule_bot,
    )

    sync_result = sync_google_calendar(
        db=db,
        user=user,
        days_ahead=days_ahead,
        auto_schedule_bot=auto_schedule_bot,
        full_sync=True,
    )
    watch_state = create_google_watch(db, user, webhook_url)
    sync_token  = create_google_sync_token(db, user)

    if sync_token:
        watch_state = crud.save_calendar_sync_state(
            db=db,
            user_id=user.id,
            calendar_id=watch_state.calendar_id,
            sync_token=sync_token,
        )

    logger.info(
        "Google Calendar watch has been created for user %s. Channel id: %s. Sync token: %s. Expires at: %s.",
        user.id,
        watch_state.channel_id,
        watch_state.sync_token,
        watch_state.expires_at,
    )
    return {"sync_result": sync_result, "watch_state": watch_state}
def create_google_watch(db, user: User, webhook_url: str, calendar_id: str = "primary"):
    channel_id = str(uuid.uuid4())
    channel_token = str(uuid.uuid4())
    response = call_google_api(
        db,
        user,
        "POST",
        GOOGLE_EVENTS_URL.format(calendar_id=quote(calendar_id, safe="")) + "/watch",
        json={
            "id": channel_id,
            "type": "web_hook",
            "address": webhook_url,
            "token": channel_token,
        },
        headers={"Content-Type": "application/json"},
    )
    data = response.json()
    expires_at = parse_google_expiry(data.get("expiration"))
    logger.info(
        "Google returned a watch response for user %s. Channel id: %s. Resource id: %s.",
        user.id,
        data.get("id"),
        data.get("resourceId"),
    )

    return crud.save_calendar_sync_state(
        db=db,
        user_id=user.id,
        calendar_id=calendar_id,
        webhook_url=webhook_url,
        channel_id=data.get("id") or channel_id,
        channel_token=channel_token,
        resource_id=data.get("resourceId"),
        resource_uri=data.get("resourceUri"),
        expires_at=expires_at,
    )


def sync_google_calendar(
    db,
    user: User,
    days_ahead: int = DEFAULT_SYNC_DAYS,
    auto_schedule_bot: bool = True,
    full_sync: bool = False,
    delete_past_events: bool = True,
):
    if not user.calendar_sync_enabled:
        logger.info("Google Calendar sync was skipped for user %s because calendar sync is disabled.", user.id)
        return {"synced": [], "skipped": [], "deleted": [], "sync_token": None}

    state = crud.get_calendar_sync_state(db, user.id)
    calendar_id = state.calendar_id if state else "primary"
    using_sync_token = bool(state and state.sync_token and not full_sync)

    if using_sync_token:
        params = {
            "syncToken": state.sync_token,
            "singleEvents": True,
            "showDeleted": True,
            "maxResults": 250,
        }
    else:
        params = {
            "singleEvents": True,
            "orderBy": "startTime",
            "maxResults": 250,
        }
        now = datetime.now(timezone.utc)
        params["timeMin"] = now.isoformat()
        params["timeMax"] = (now + timedelta(days=days_ahead)).isoformat()

    logger.info(
        "Google Calendar sync has started for user %s. Calendar: %s. Full sync: %s. Using saved sync token: %s. Current sync token: %s.",
        user.id,
        calendar_id,
        full_sync,
        using_sync_token,
        state.sync_token if state else None,
    )

    synced = []
    skipped = []
    deleted = []
    next_page_token = None
    next_sync_token = None

    while True:
        request_params = dict(params)
        if next_page_token:
            request_params["pageToken"] = next_page_token

        try:
            response = call_google_api(
                db,
                user,
                "GET",
                GOOGLE_EVENTS_URL.format(calendar_id=quote(calendar_id, safe="")),
                params=request_params,
            )
        except requests.HTTPError as exc:
            if exc.response is not None:
                if exc.response.status_code == 403 and "ACCESS_TOKEN_SCOPE_INSUFFICIENT" in exc.response.text:
                    logger.warning("Google Calendar API returned 403 due to insufficient scopes. Disabling calendar sync for user %s.", user.id)
                    crud.set_calendar_sync_enabled(db, user.id, False)
                    return
                if exc.response.status_code in (410, 403) and state and state.sync_token:
                    logger.warning("Google Calendar API returned %s. Wiping sync token and attempting full sync.", exc.response.status_code)
                    crud.save_calendar_sync_state(db, user.id, calendar_id=calendar_id, sync_token="")
                    return sync_google_calendar(
                        db=db,
                        user=user,
                        days_ahead=days_ahead,
                        auto_schedule_bot=auto_schedule_bot,
                        full_sync=True,
                        delete_past_events=delete_past_events,
                    )
            raise
  
        payload = response.json()
        logger.info(
            "Google Calendar returned %s events for user %s. Next page token: %s. Next sync token: %s.",
            len(payload.get("items", [])),
            user.id,
            payload.get("nextPageToken"),
            payload.get("nextSyncToken"),
        )
        for event in payload.get("items", []):
            result = sync_one_google_event(
                db,
                user,
                event,
                auto_schedule_bot=auto_schedule_bot,
                delete_past_events=delete_past_events,
            )
            if result["action"] == "synced":
                synced.append(result["data"])
            elif result["action"] == "deleted":
                deleted.append(result["data"])
            else:
                skipped.append(result["data"])

        next_page_token = payload.get("nextPageToken")
        next_sync_token = payload.get("nextSyncToken") or next_sync_token
        if not next_page_token:
            break

    crud.save_calendar_sync_state(
        db=db,
        user_id=user.id,
        calendar_id=calendar_id,
        sync_token=next_sync_token or (state.sync_token if state else None),
    )
    window_start = datetime.now(timezone.utc)
    window_end = window_start + timedelta(days=RECURRING_MEETING_WINDOW_DAYS)
    removed_candidates = crud.cleanup_recurring_candidates(db, user.id, window_start, window_end)
    crud.refresh_recurring_candidate_visibility(db, user.id, window_start, window_end)
    if removed_candidates:
        logger.info("Removed %s stale recurring meeting candidates for user %s.", removed_candidates, user.id)

    return {
        "synced": synced,
        "skipped": skipped,
        "deleted": deleted,
        "sync_token": next_sync_token or (state.sync_token if state else None),
    }


def handle_google_notification(
    db,
    channel_id: str,
    channel_token: str | None,
    resource_state: str | None,
    message_number: str | None,
):
    logger.info(
        "A Google Calendar webhook notification was received. Channel id: %s. Resource state: %s. Message number: %s.",
        channel_id,
        resource_state,
        message_number,
    )
    state = crud.get_calendar_sync_state_by_channel(db, channel_id)
    if not state:
        logger.info("The Google Calendar webhook for channel %s was ignored because the watch channel is unknown or expired.", channel_id)
        return {
            "synced": [],
            "skipped": [{"channel_id": channel_id, "reason": "unknown-or-old-watch-channel"}],
            "deleted": [],
            "resource_state": resource_state,
        }
    if state.channel_token and channel_token != state.channel_token:
        logger.warning("The Google Calendar webhook for channel %s was ignored because the channel token did not match.", channel_id)
        return {
            "synced": [],
            "skipped": [{"channel_id": channel_id, "reason": "watch-channel-token-mismatch"}],
            "deleted": [],
            "resource_state": resource_state,
        }
    logger.info(
        "The Google Calendar webhook was matched to user %s. Calendar: %s. Sync token: %s.",
        state.user_id,
        state.calendar_id,
        state.sync_token,
    )

    crud.save_calendar_sync_state(
        db=db,
        user_id=state.user_id,
        calendar_id=state.calendar_id,
        last_message_number=message_number,
    )

    if resource_state == "sync":
        return {"synced": [], "skipped": [], "deleted": [], "resource_state": resource_state}

    user = db.query(User).filter(User.id == state.user_id).first()
    if not user:
        raise ValueError("User not found for Google watch channel")
    if not user.calendar_sync_enabled:
        logger.info("The Google Calendar webhook was skipped for user %s because calendar sync is disabled.", user.id)
        return {"synced": [], "skipped": [], "deleted": [], "resource_state": resource_state}

    result = sync_google_calendar(
        db=db,
        user=user,
        days_ahead=DEFAULT_SYNC_DAYS,
        auto_schedule_bot=True,
        full_sync=False,
    )
    result["resource_state"] = resource_state
    if not result["sync_token"]:
        result["sync_token"] = create_google_sync_token(db, user)
    return result


def create_google_sync_token(db, user: User, calendar_id: str = "primary") -> str | None:
    logger.info("A fresh Google Calendar sync token is being created for user %s and calendar %s.", user.id, calendar_id)
    next_page_token = None
    next_sync_token = None

    while True:
        params = {
            "singleEvents": True,
            "showDeleted": True,
            "maxResults": 250,
        }
        if next_page_token:
            params["pageToken"] = next_page_token

        response = call_google_api(
            db,
            user,
            "GET",
            GOOGLE_EVENTS_URL.format(calendar_id=quote(calendar_id, safe="")),
            params=params,
        )
        payload = response.json()
        next_page_token = payload.get("nextPageToken")
        next_sync_token = payload.get("nextSyncToken") or next_sync_token
        if not next_page_token:
            break

    if next_sync_token:
        crud.save_calendar_sync_state(
            db=db,
            user_id=user.id,
            calendar_id=calendar_id,
            sync_token=next_sync_token,
        )
        logger.info("A new Google Calendar sync token was saved for user %s. Sync token: %s.", user.id, next_sync_token)
    else:
        logger.warning("Google did not return a new sync token for user %s.", user.id)

    return next_sync_token


def sync_one_google_event(
    db,
    user: User,
    event: dict,
    auto_schedule_bot: bool = True,
    delete_past_events: bool = True,
):
    event_id = event.get("id")
    logger.info(
        "Google Calendar event processing has started for user %s. Event id: %s. Title: %s. Status: %s.",
        user.id,
        event_id,
        event.get("summary"),
        event.get("status"),
    )
    if not event_id:
        return {"action": "skipped", "data": {"calendar_event_id": None, "reason": "missing-event-id"}}

    if event.get("status") == "cancelled":
        meeting = crud.get_meeting_by_calendar_event_id(db, user.id, event_id)
        if meeting and meeting.recall_bot_id:
            try:
                stop_result = recall_service.stop_bot(meeting.recall_bot_id)
                logger.info(
                    "Recall bot stop was requested for cancelled Google Calendar event %s. Meeting id: %s. Bot id: %s. Method: %s. Previous bot status: %s.",
                    event_id,
                    meeting.id,
                    meeting.recall_bot_id,
                    stop_result.get("method"),
                    stop_result.get("bot_status"),
                )
            except requests.RequestException as exc:
                logger.warning(
                    "Recall bot stop failed for cancelled Google Calendar event %s. Meeting id: %s. Bot id: %s. Error: %s.",
                    event_id,
                    meeting.id,
                    meeting.recall_bot_id,
                    exc,
                )
        crud.delete_recurring_candidate(db, user.id, event_id)
        crud.delete_recurring_meeting_invite(db, user.id, event_id)
        crud.delete_non_recurring_meeting_invite(db, user.id, event_id)
        deleted = crud.delete_calendar_meeting(db, user.id, event_id)
        return {"action": "deleted", "data": {"calendar_event_id": event_id, "deleted": deleted}}

    start_time = get_event_start_time(event)
    end_time = get_event_end_time(event)
    if not start_time:
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "missing-or-invalid-start-time"},
        }

    meeting_link = find_meeting_link(event)
    if not meeting_link:
        logger.info("Google Calendar event %s was skipped because no supported meeting link was found.", event_id)
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "no-supported-meeting-link"},
        }

    platform = get_meeting_platform(meeting_link)
    if not platform:
        logger.info("Google Calendar event %s was skipped because the meeting link is not supported: %s.", event_id, meeting_link)
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "unsupported-meeting-link"},
        }
    logger.info(
        "Google Calendar event %s was parsed successfully. Platform: %s. Start time: %s. End time: %s.",
        event_id,
        platform,
        start_time,
        end_time,
    )
    now = datetime.now(timezone.utc)

    if is_recurring_event(event):
        if not is_organizer_user(user, event):
            return _sync_recurring_guest_invite(
                db=db,
                user=user,
                event=event,
                event_id=event_id,
                meet_link=meeting_link,
                platform=platform,
                start_time=start_time,
            )
        return _sync_recurring_google_event(
            db=db,
            user=user,
            event=event,
            event_id=event_id,
            meet_link=meeting_link,
            platform=platform,
            start_time=start_time,
            end_time=end_time,
            auto_schedule_bot=auto_schedule_bot,
            delete_past_events=delete_past_events,
        )

    if not is_organizer_user(user, event):
        return _sync_non_recurring_guest_invite(
            db=db,
            user=user,
            event=event,
            event_id=event_id,
            meet_link=meeting_link,
            platform=platform,
            start_time=start_time,
        )

    if not _is_future_calendar_event(start_time, now):
        deleted = crud.delete_calendar_meeting(db, user.id, event_id)
        logger.info(
            "Google Calendar event %s was skipped because it is in the past. Existing meeting deleted: %s. Start time: %s. End time: %s.",
            event_id,
            deleted,
            start_time,
            end_time,
        )
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "non-recurring-past-event"},
        }

    window_end = now + timedelta(days=DEFAULT_SYNC_DAYS)
    if start_time > window_end:
        existing_meeting = crud.get_meeting_by_calendar_event_id(db, user.id, event_id)
        if existing_meeting and existing_meeting.recall_bot_id:
            try:
                recall_service.stop_bot(existing_meeting.recall_bot_id)
                logger.info("Bot %s stopped because meeting %s was rescheduled outside the window.", existing_meeting.recall_bot_id, existing_meeting.id)
            except Exception as e:
                logger.warning("Failed to stop bot %s: %s", existing_meeting.recall_bot_id, e)
                
        deleted = crud.delete_calendar_meeting(db, user.id, event_id)
        logger.info(
            "Google Calendar event %s was skipped because it is outside the next %s-day window. Existing meeting deleted: %s.",
            event_id,
            DEFAULT_SYNC_DAYS,
            deleted,
        )
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "non-recurring-event-outside-window"},
        }

    existing_meeting = crud.get_meeting_by_calendar_event_id(db, user.id, event_id)
    previous_scheduled_at = existing_meeting.scheduled_at if existing_meeting else None
    previous_meet_link = existing_meeting.meet_link if existing_meeting else None
    had_existing_bot = bool(existing_meeting and existing_meeting.recall_bot_id)

    meeting = crud.save_calendar_meeting(
        db=db,
        user_id=user.id,
        calendar_event_id=event_id,
        title=event.get("summary"),
        meet_link=meeting_link,
        scheduled_at=start_time,
        platform=platform,
    )
    crud.link_non_recurring_meeting_invites_to_meeting(
        db,
        host_user_id=user.id,
        google_event_id=event_id,
        meeting_id=meeting.id,
    )

    bot_created = False
    bot_join_mode = None

    if auto_schedule_bot and not meeting.recall_bot_id:
        join_at = format_join_time(start_time)
        bot_join_mode = "scheduled"

        logger.info(
            "Recall bot creation has started for meeting %s from Google Calendar event %s. Join mode: %s. Join time: %s.",
            meeting.id,
            event_id,
            bot_join_mode,
            join_at,
        )
        bot_response = recall_service.create_bot(
            meeting.meet_link,
            metadata={"meeting_id": str(meeting.id), "user_id": str(user.id)},
            join_at=join_at,
        )
        meeting = crud.set_meeting_recall_bot(db, meeting.id, bot_response)
        bot_created = True
        logger.info("Recall bot was created for meeting %s from Google Calendar event %s. Bot id: %s.", meeting.id, event_id, meeting.recall_bot_id)
    elif auto_schedule_bot and had_existing_bot and _should_recreate_scheduled_bot(
        previous_scheduled_at=previous_scheduled_at,
        new_scheduled_at=start_time,
        previous_meet_link=previous_meet_link,
        new_meet_link=meeting.meet_link,
    ):
        logger.info(
            "Recall bot reschedule has started for meeting %s from Google Calendar event %s. Previous scheduled time: %s. New scheduled time: %s.",
            meeting.id,
            event_id,
            previous_scheduled_at,
            start_time,
        )
        meeting = _recreate_scheduled_bot(
            db=db,
            meeting=meeting,
            user_id=user.id,
            join_time=start_time,
        )
        bot_created = True
        bot_join_mode = "rescheduled"
        logger.info(
            "Recall bot was rescheduled for meeting %s from Google Calendar event %s. New bot id: %s. New join time: %s.",
            meeting.id,
            event_id,
            meeting.recall_bot_id,
            meeting.recall_join_at,
        )
    elif auto_schedule_bot and meeting.recall_bot_id:
        logger.info("Recall bot creation was skipped for Google Calendar event %s because the meeting already has a bot.", event_id)

    return {
        "action": "synced",
        "data": {
            "calendar_event_id": event_id,
            "meeting_id": meeting.id,
            "title": meeting.title,
            "meet_link": meeting.meet_link,
            "platform": meeting.platform,
            "scheduled_at": meeting.scheduled_at.isoformat() if meeting.scheduled_at else None,
            "bot_created": bot_created,
            "recall_bot_id": meeting.recall_bot_id,
            "bot_join_mode": bot_join_mode,
        },
    }


def _sync_recurring_guest_invite(
    db,
    user: User,
    event: dict,
    event_id: str,
    meet_link: str,
    platform: str,
    start_time: datetime,
):
    now = datetime.now(timezone.utc)
    window_end = now + timedelta(days=RECURRING_MEETING_WINDOW_DAYS)

    if start_time < now or start_time > window_end:
        crud.delete_recurring_meeting_invite(db, user.id, event_id)
        logger.info(
            "Recurring guest invitation %s was skipped for user %s because it is outside the next %s-day window.",
            event_id,
            user.id,
            RECURRING_MEETING_WINDOW_DAYS,
        )
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "recurring-guest-event-outside-window"},
        }

    guest_rsvp_status = get_guest_rsvp_status(event, user.email)
    organizer_email = get_organizer_email(event)
    host_user = crud.get_user_by_email(db, organizer_email)
    recurring_series_id = get_recurring_series_id(event)
    host_user_id = host_user.id if host_user else None

    # Upsert the invite first
    invite = crud.upsert_recurring_meeting_invite(
        db=db,
        host_user_id=host_user_id,
        guest_user_id=user.id,
        recurring_series_id=recurring_series_id,
        google_event_id=event_id,
        title=event.get("summary"),
        meet_link=meet_link,
        platform=platform,
        scheduled_at=start_time,
        guest_rsvp_status=guest_rsvp_status,
        organizer_email=organizer_email,
    )
    crud.cleanup_recurring_meeting_invites(db, user.id, now, window_end)

    bot_created = False
    recall_bot_id = None
    bot_join_mode = None

    if host_user:
        # The host's settings dictate the bot. We do not schedule a bot from the guest side.
        logger.info(
            "Recurring guest invitation %s was saved for user %s. Host user: %s. Guest RSVP: %s. No bot created from guest side (Scenario 2).",
            event_id,
            user.id,
            host_user.id,
            guest_rsvp_status,
        )
        return {
            "action": "synced" if guest_rsvp_status == "accepted" else "skipped",
            "data": {
                "calendar_event_id": event_id,
                "reason": "recurring-guest-invite-saved" if guest_rsvp_status == "accepted" else "recurring-guest-rsvp-not-accepted",
                "invite_id": invite.id,
                "bot_created": False,
            },
        }
    else:
        # If guest RSVP is yes, we trigger the bot.
        if guest_rsvp_status == "accepted":
            existing_meeting = crud.get_meeting_by_meet_link_and_time(db, meet_link, start_time)
            
            if existing_meeting and existing_meeting.status != "abort":
                logger.info(
                    "Recurring guest invitation %s for user %s found existing bot %s. Linking to existing meeting.",
                    event_id,
                    user.id,
                    existing_meeting.recall_bot_id,
                )
                invite.meeting_id = existing_meeting.id
                db.commit()
                bot_created = False
                recall_bot_id = existing_meeting.recall_bot_id
            else:
                # Schedule bot for this guest using a host-less Meeting
                join_at = format_join_time(start_time)
                bot_join_mode = "scheduled"

                if existing_meeting and existing_meeting.status == "abort":
                    logger.info("Resurrecting aborted recurring meeting %s for guest %s.", existing_meeting.id, user.id)
                    meeting = existing_meeting
                    meeting.status = "scheduled"
                    meeting.host_rsvp_status = "accepted"
                    db.commit()
                else:
                    # Create a host-less Meeting so it stays in the Invited section
                    meeting = crud.Meeting(
                        title=event.get("summary"),
                        calendar_event_id=event_id,
                        meet_link=meet_link,
                        platform=platform,
                        user_id=None,  # Host-less
                        scheduled_at=start_time,
                        is_recurring=True,
                        recurring_series_id=recurring_series_id,
                        source_event_id=event_id,
                        host_rsvp_status="accepted",  # Acting as accepted host
                        app_toggle_enabled=True,
                    )
                    db.add(meeting)
                    db.commit()
                    db.refresh(meeting)

                # Link invite
                invite.meeting_id = meeting.id
                db.commit()

                # Create Recall Bot
                logger.info(
                    "Guest user %s is scheduling bot for recurring event %s (Scenario 1). Join time: %s",
                    user.id,
                    event_id,
                    join_at,
                )
                bot_response = recall_service.create_bot(
                    meeting.meet_link,
                    metadata={"meeting_id": str(meeting.id), "guest_user_id": str(user.id)},
                    join_at=join_at,
                )
                crud.set_meeting_recall_bot(db, meeting.id, bot_response)
                bot_created = True
                recall_bot_id = meeting.recall_bot_id
                
            return {
                "action": "synced",
                "data": {
                    "calendar_event_id": event_id,
                    "reason": "recurring-guest-bot-scheduled",
                    "invite_id": invite.id,
                    "bot_created": bot_created,
                    "recall_bot_id": recall_bot_id,
                    "bot_join_mode": bot_join_mode,
                },
            }
        else:
            logger.info(
                "Recurring guest invitation %s was saved for user %s but RSVP is not accepted. No bot created.",
                event_id,
                user.id,
            )
            if invite.meeting_id:
                accepted_count = crud.count_accepted_guests_for_meeting(db, invite.meeting_id)
                if accepted_count == 0:
                    meeting = crud.get_meeting_by_id(db, invite.meeting_id)
                    if meeting and not meeting.user_id and meeting.recall_bot_id:
                        try:
                            recall_service.stop_bot(meeting.recall_bot_id)
                            logger.info(
                                "Bot %s for recurring meeting %s was aborted because all app guests declined.",
                                meeting.recall_bot_id,
                                meeting.id,
                            )
                        except requests.RequestException as exc:
                            logger.warning(
                                "Failed to stop bot %s for recurring meeting %s: %s", meeting.recall_bot_id, meeting.id, exc
                            )
                        
                        meeting.recall_bot_id = None
                        meeting.bot_status = None
                        meeting.bot_payload = None
                        meeting.recall_join_at = None
                        crud.mark_meeting_abort(db, meeting.id)

            return {
                "action": "skipped",
                "data": {
                    "calendar_event_id": event_id,
                    "reason": "recurring-guest-rsvp-not-accepted",
                    "invite_id": invite.id,
                },
            }


def _sync_non_recurring_guest_invite(
    db,
    user: User,
    event: dict,
    event_id: str,
    meet_link: str,
    platform: str,
    start_time: datetime,
):
    now = datetime.now(timezone.utc)
    window_end = now + timedelta(days=DEFAULT_SYNC_DAYS)

    if start_time < now or start_time > window_end:
        crud.delete_non_recurring_meeting_invite(db, user.id, event_id)
        logger.info(
            "Non-recurring guest invitation %s was skipped for user %s because it is outside the next %s-day window.",
            event_id,
            user.id,
            DEFAULT_SYNC_DAYS,
        )
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "non-recurring-guest-event-outside-window"},
        }

    guest_rsvp_status = get_guest_rsvp_status(event, user.email)
    organizer_email = get_organizer_email(event)
    host_user = crud.get_user_by_email(db, organizer_email)
    host_user_id = host_user.id if host_user else None

    # Upsert the invite first
    invite = crud.upsert_non_recurring_meeting_invite(
        db=db,
        host_user_id=host_user_id,
        guest_user_id=user.id,
        google_event_id=event_id,
        title=event.get("summary"),
        meet_link=meet_link,
        platform=platform,
        scheduled_at=start_time,
        guest_rsvp_status=guest_rsvp_status,
        organizer_email=organizer_email,
    )
    crud.cleanup_non_recurring_meeting_invites(db, user.id, now, window_end)

    bot_created = False
    recall_bot_id = None
    bot_join_mode = None

    if host_user:
        # The host's settings dictate the bot. We do not schedule a bot from the guest side.
        logger.info(
            "Non-recurring guest invitation %s was saved for user %s. Host user: %s. Guest RSVP: %s. No bot created from guest side.",
            event_id,
            user.id,
            host_user.id,
            guest_rsvp_status,
        )
        return {
            "action": "synced",
            "data": {
                "calendar_event_id": event_id,
                "reason": "non-recurring-guest-invite-saved",
                "invite_id": invite.id,
                "bot_created": False,
            },
        }
    else:
        # If guest RSVP is yes, we trigger the bot.
        if guest_rsvp_status == "accepted":
            existing_meeting = crud.get_meeting_by_meet_link_and_time(db, meet_link, start_time)
            
            if existing_meeting and existing_meeting.status != "abort":
                logger.info(
                    "Non-recurring guest invitation %s for user %s found existing bot %s. Linking to existing meeting.",
                    event_id,
                    user.id,
                    existing_meeting.recall_bot_id,
                )
                invite.meeting_id = existing_meeting.id
                db.commit()
                bot_created = False
                recall_bot_id = existing_meeting.recall_bot_id
            else:
                # Schedule bot for this guest using a host-less Meeting
                join_at = format_join_time(start_time)
                bot_join_mode = "scheduled"
    
                if existing_meeting and existing_meeting.status == "abort":
                    logger.info("Resurrecting aborted non-recurring meeting %s for guest %s.", existing_meeting.id, user.id)
                    meeting = existing_meeting
                    meeting.status = "scheduled"
                    meeting.host_rsvp_status = "accepted"
                    db.commit()
                else:
                    # Create a host-less Meeting so it stays in the Invited section
                    meeting = crud.Meeting(
                        title=event.get("summary"),
                        calendar_event_id=event_id,
                        meet_link=meet_link,
                        platform=platform,
                        user_id=None,  # Host-less
                        scheduled_at=start_time,
                        is_recurring=False,
                        source_event_id=event_id,
                        host_rsvp_status="accepted",
                        app_toggle_enabled=True,
                    )
                    db.add(meeting)
                    db.commit()
                    db.refresh(meeting)
    
                # Link invite
                invite.meeting_id = meeting.id
                db.commit()
    
                # Create Recall Bot
                logger.info(
                    "Guest user %s is scheduling bot for non-recurring event %s (Scenario 3). Join time: %s",
                    user.id,
                    event_id,
                    join_at,
                )
                bot_response = recall_service.create_bot(
                    meeting.meet_link,
                    metadata={"meeting_id": str(meeting.id), "guest_user_id": str(user.id)},
                    join_at=join_at,
                )
                crud.set_meeting_recall_bot(db, meeting.id, bot_response)
                bot_created = True
                recall_bot_id = meeting.recall_bot_id
                
            return {
                "action": "synced",
                "data": {
                    "calendar_event_id": event_id,
                    "reason": "non-recurring-guest-bot-scheduled",
                    "invite_id": invite.id,
                    "bot_created": bot_created,
                    "recall_bot_id": recall_bot_id,
                    "bot_join_mode": bot_join_mode,
                },
            }
        else:
            logger.info(
                "Non-recurring guest invitation %s was saved for user %s but RSVP is not accepted. No bot created.",
                event_id,
                user.id,
            )
            if invite.meeting_id:
                accepted_count = crud.count_accepted_guests_for_meeting(db, invite.meeting_id)
                if accepted_count == 0:
                    meeting = crud.get_meeting_by_id(db, invite.meeting_id)
                    if meeting and not meeting.user_id and meeting.recall_bot_id:
                        try:
                            recall_service.stop_bot(meeting.recall_bot_id)
                            logger.info(
                                "Bot %s for non-recurring meeting %s was aborted because all app guests declined.",
                                meeting.recall_bot_id,
                                meeting.id,
                            )
                        except requests.RequestException as exc:
                            logger.warning(
                                "Failed to stop bot %s for non-recurring meeting %s: %s", meeting.recall_bot_id, meeting.id, exc
                            )
                        
                        meeting.recall_bot_id = None
                        meeting.bot_status = None
                        meeting.bot_payload = None
                        meeting.recall_join_at = None
                        crud.mark_meeting_abort(db, meeting.id)

            return {
                "action": "skipped",
                "data": {
                    "calendar_event_id": event_id,
                    "reason": "non-recurring-guest-rsvp-not-accepted",
                    "invite_id": invite.id,
                    "bot_created": False,
                },
            }


def _sync_recurring_google_event(
    db,
    user: User,
    event: dict,
    event_id: str,
    meet_link: str,
    platform: str,
    start_time: datetime,
    end_time: datetime | None,
    auto_schedule_bot: bool,
    delete_past_events: bool = True,
):
    now = datetime.now(timezone.utc)
    window_end = now + timedelta(days=RECURRING_MEETING_WINDOW_DAYS)

    if start_time <= now:
        crud.set_recurring_candidate_status(
            db,
            crud.get_recurring_candidate_by_event_id(db, user.id, event_id).id,
            "expired",
        ) if crud.get_recurring_candidate_by_event_id(db, user.id, event_id) else None
        deleted = crud.delete_calendar_meeting(db, user.id, event_id) if delete_past_events else False
        logger.info(
            "Recurring Google Calendar event %s was skipped because it is in the past. Existing meeting deleted: %s.",
            event_id,
            deleted,
        )
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "recurring-event-in-past"},
        }

    if start_time > window_end:
        logger.info(
            "Recurring Google Calendar event %s was skipped because it is outside the next %s-day window.",
            event_id,
            RECURRING_MEETING_WINDOW_DAYS,
        )
        return {
            "action": "skipped",
            "data": {"calendar_event_id": event_id, "reason": "recurring-event-outside-window"},
        }

    recurring_series_id = get_recurring_series_id(event)
    host_rsvp_status = get_organizer_rsvp_status(event)
    organizer_email = get_organizer_email(event)
    existing_candidate = crud.get_recurring_candidate_by_event_id(db, user.id, event_id)
    app_toggle_enabled = existing_candidate.app_toggle_enabled if existing_candidate else False
    candidate_status = get_recurring_candidate_status(
        app_toggle_enabled=app_toggle_enabled,
        host_rsvp_status=host_rsvp_status,
        linked_meeting_id=existing_candidate.linked_meeting_id if existing_candidate else None,
    )
    candidate = crud.upsert_recurring_meeting_candidate(
        db=db,
        user_id=user.id,
        recurring_series_id=recurring_series_id,
        google_event_id=event_id,
        title=event.get("summary"),
        meet_link=meet_link,
        platform=platform,
        scheduled_at=start_time,
        host_rsvp_status=host_rsvp_status,
        organizer_email=organizer_email,
        app_toggle_enabled=app_toggle_enabled,
        bot_permission_status=candidate_status,
        payload=event,
    )
    crud.refresh_recurring_candidate_visibility(db, user.id, now, window_end)

    decision = ensure_recurring_candidate_bot_state(
        db=db,
        user=user,
        candidate=candidate,
        auto_schedule_bot=auto_schedule_bot,
    )
    return {
        "action": "synced" if decision.get("meeting_id") else "skipped",
        "data": {
            "calendar_event_id": event_id,
            "meeting_id": decision.get("meeting_id"),
            "title": candidate.title,
            "meet_link": candidate.meet_link,
            "platform": candidate.platform,
            "scheduled_at": candidate.scheduled_at.isoformat() if candidate.scheduled_at else None,
            "bot_created": decision.get("bot_created", False),
            "recall_bot_id": decision.get("recall_bot_id"),
            "bot_join_mode": decision.get("bot_join_mode"),
            "recurring_candidate_id": candidate.id,
            "reason": decision.get("reason"),
        },
    }


def get_recurring_candidate_status(
    app_toggle_enabled: bool,
    host_rsvp_status: str | None,
    linked_meeting_id: int | None = None,
) -> str:
    if linked_meeting_id:
        return "confirmed"
    if not app_toggle_enabled and host_rsvp_status == "accepted":
        return "waiting_for_app_confirmation"
    if app_toggle_enabled and host_rsvp_status != "accepted":
        return "waiting_for_calendar_yes"
    if app_toggle_enabled and host_rsvp_status == "accepted":
        return "ready"
    return "pending"


def ensure_recurring_candidate_bot_state(
    db: Session,
    user: User,
    candidate: RecurringMeetingCandidate,
    auto_schedule_bot: bool = True,
) -> dict:
    now = datetime.now(timezone.utc)

    if candidate.host_rsvp_status != "accepted":
        if candidate.linked_meeting_id:
            linked_meeting = crud.get_meeting_by_id(db, candidate.linked_meeting_id)
            if linked_meeting and linked_meeting.scheduled_at and linked_meeting.scheduled_at > now:
                linked_meeting.host_rsvp_status = candidate.host_rsvp_status
                if linked_meeting.recall_bot_id:
                    try:
                        stop_result = recall_service.stop_bot(linked_meeting.recall_bot_id)
                        logger.info(
                            "Recurring meeting %s bot stop was requested after organizer RSVP changed. Method: %s. Previous bot status: %s.",
                            linked_meeting.id,
                            stop_result.get("method"),
                            stop_result.get("bot_status"),
                        )
                    except requests.RequestException as exc:
                        logger.warning(
                            "Recurring meeting %s could not stop the Recall bot after organizer RSVP changed. Error: %s.",
                            linked_meeting.id,
                            exc,
                        )
                linked_meeting.recall_bot_id = None
                linked_meeting.bot_status = None
                linked_meeting.bot_payload = None
                linked_meeting.recall_join_at = None
                crud.mark_meeting_abort(db, linked_meeting.id)
                crud.unlink_recurring_candidate_meeting(db, candidate.id)
                candidate = crud.set_recurring_candidate_status(
                    db,
                    candidate.id,
                    "waiting_for_calendar_yes" if candidate.app_toggle_enabled else "pending",
                )
        return {"reason": "recurring-organizer-rsvp-not-accepted"}

    if not candidate.app_toggle_enabled:
        crud.set_recurring_candidate_status(db, candidate.id, "waiting_for_app_confirmation")
        return {"reason": "recurring-app-toggle-off"}

    if candidate.scheduled_at <= now:
        deleted = crud.delete_calendar_meeting(db, user.id, candidate.google_event_id)
        if deleted and candidate.linked_meeting_id:
            crud.unlink_recurring_candidate_meeting(db, candidate.id)
        crud.set_recurring_candidate_status(db, candidate.id, "expired")
        logger.info(
            "Recurring candidate %s was not converted into a meeting because it is in the past. Existing meeting deleted: %s. Scheduled at: %s.",
            candidate.id,
            deleted,
            candidate.scheduled_at,
        )
        return {
            "reason": "recurring-meeting-in-past",
            "bot_created": False,
            "recall_bot_id": None,
            "bot_join_mode": None,
        }

    previous_scheduled_at = None
    previous_meet_link = None
    had_existing_bot = False
    if candidate.linked_meeting_id:
        existing_linked_meeting = crud.get_meeting_by_id(db, candidate.linked_meeting_id)
        if existing_linked_meeting:
            previous_scheduled_at = existing_linked_meeting.scheduled_at
            previous_meet_link = existing_linked_meeting.meet_link
            had_existing_bot = bool(existing_linked_meeting.recall_bot_id)

    meeting = crud.save_calendar_meeting(
        db=db,
        user_id=user.id,
        calendar_event_id=candidate.google_event_id,
        title=candidate.title,
        meet_link=candidate.meet_link,
        scheduled_at=candidate.scheduled_at,
        platform=candidate.platform,
        is_recurring=True,
        recurring_series_id=candidate.recurring_series_id,
        source_event_id=candidate.google_event_id,
        host_rsvp_status=candidate.host_rsvp_status,
        app_toggle_enabled=candidate.app_toggle_enabled,
    )
    crud.set_recurring_candidate_linked_meeting(db, candidate.id, meeting.id)
    crud.link_recurring_meeting_invites_to_meeting(
        db,
        host_user_id=user.id,
        google_event_id=candidate.google_event_id,
        meeting_id=meeting.id,
    )

    bot_created = False
    bot_join_mode = None

    if auto_schedule_bot and not meeting.recall_bot_id:
        join_at = format_join_time(candidate.scheduled_at)
        bot_join_mode = "scheduled"

        logger.info(
            "Recall bot creation has started for recurring meeting %s from Google Calendar event %s. Join mode: %s. Join time: %s.",
            meeting.id,
            candidate.google_event_id,
            bot_join_mode,
            join_at,
        )
        bot_response = recall_service.create_bot(
            meeting.meet_link,
            metadata={"meeting_id": str(meeting.id), "user_id": str(user.id)},
            join_at=join_at,
        )
        meeting = crud.set_meeting_recall_bot(db, meeting.id, bot_response)
        bot_created = True
    elif auto_schedule_bot and had_existing_bot and _should_recreate_scheduled_bot(
        previous_scheduled_at=previous_scheduled_at,
        new_scheduled_at=candidate.scheduled_at,
        previous_meet_link=previous_meet_link,
        new_meet_link=meeting.meet_link,
    ):
        logger.info(
            "Recall bot reschedule has started for recurring meeting %s from Google Calendar event %s. Previous scheduled time: %s. New scheduled time: %s.",
            meeting.id,
            candidate.google_event_id,
            previous_scheduled_at,
            candidate.scheduled_at,
        )
        meeting = _recreate_scheduled_bot(
            db=db,
            meeting=meeting,
            user_id=user.id,
            join_time=candidate.scheduled_at,
        )
        bot_created = True
        bot_join_mode = "rescheduled"

    crud.set_recurring_candidate_status(db, candidate.id, "confirmed")
    return {
        "meeting_id": meeting.id,
        "bot_created": bot_created,
        "recall_bot_id": meeting.recall_bot_id,
        "bot_join_mode": bot_join_mode,
    }


def _normalize_utc_datetime(value: datetime | None) -> datetime | None:
    if not value:
        return None
    if value.tzinfo is None:
        return value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)


def _should_recreate_scheduled_bot(
    previous_scheduled_at: datetime | None,
    new_scheduled_at: datetime | None,
    previous_meet_link: str | None,
    new_meet_link: str | None,
) -> bool:
    normalized_previous = _normalize_utc_datetime(previous_scheduled_at)
    normalized_new = _normalize_utc_datetime(new_scheduled_at)
    if normalized_previous != normalized_new:
        return True
    return (previous_meet_link or "").strip() != (new_meet_link or "").strip()


def _recreate_scheduled_bot(
    db: Session,
    meeting,
    user_id: int,
    join_time: datetime,
):
    old_bot_id = meeting.recall_bot_id
    if old_bot_id:
        recall_service.stop_bot(old_bot_id)

    meeting.recall_bot_id = None
    meeting.bot_status = None
    meeting.bot_payload = None
    meeting.recall_join_at = None
    meeting.status = "scheduled" if join_time > datetime.now(timezone.utc) else "pending"
    db.commit()
    db.refresh(meeting)

    bot_response = recall_service.create_bot(
        meeting.meet_link,
        metadata={"meeting_id": str(meeting.id), "user_id": str(user_id)},
        join_at=format_join_time(join_time),
    )
    return crud.set_meeting_recall_bot(db, meeting.id, bot_response)


def get_event_start_time(event: dict) -> datetime | None:
    start_data = event.get("start") or {}
    start_value = start_data.get("dateTime")
    if not start_value:
        return None
    parsed = crud.parse_iso_datetime(start_value)
    if not parsed:
        return None
    return parsed.astimezone(timezone.utc)


def get_event_end_time(event: dict) -> datetime | None:
    end_data = event.get("end") or {}
    end_value = end_data.get("dateTime")
    if not end_value:
        return None
    parsed = crud.parse_iso_datetime(end_value)
    if not parsed:
        return None
    return parsed.astimezone(timezone.utc)


def find_meeting_link(event: dict) -> str | None:
    for value in [
        event.get("hangoutLink"),
        find_link_in_text(event.get("location")),
        find_link_in_text(event.get("description")),
    ]:
        if value:
            return value
    return None


def is_recurring_event(event: dict) -> bool:
    return bool(event.get("recurringEventId") or event.get("recurrence"))


def get_recurring_series_id(event: dict) -> str:
    return str(event.get("recurringEventId") or event.get("id"))


def get_organizer_email(event: dict) -> str | None:
    organizer = event.get("organizer") or {}
    email = organizer.get("email")
    if isinstance(email, str) and email.strip():
        return email.strip().lower()
    return None


def get_organizer_rsvp_status(event: dict) -> str | None:
    organizer_email = get_organizer_email(event)
    attendees = event.get("attendees") or []
    for attendee in attendees:
        if attendee.get("self") is True:
            return attendee.get("responseStatus")
        attendee_email = (attendee.get("email") or "").strip().lower()
        if organizer_email and attendee_email == organizer_email:
            return attendee.get("responseStatus")
    if event.get("responseStatus"):
        return event.get("responseStatus")
    return None


def get_guest_rsvp_status(event: dict, guest_email: str | None) -> str | None:
    if not guest_email:
        return None
    normalized_email = guest_email.strip().lower()
    attendees = event.get("attendees") or []
    for attendee in attendees:
        attendee_email = (attendee.get("email") or "").strip().lower()
        if attendee_email == normalized_email:
            return attendee.get("responseStatus")
    return None


def is_organizer_user(user: User, event: dict) -> bool:
    organizer_email = get_organizer_email(event)
    if organizer_email and user.email and organizer_email == user.email.strip().lower():
        return True
    attendees = event.get("attendees") or []
    for attendee in attendees:
        if attendee.get("self") is True:
            return bool(attendee.get("organizer"))
    return False


def find_link_in_text(text: str | None) -> str | None:
    if not text:
        return None

    normalized_text = html.unescape(text)
    candidates = re.findall(r"https?://[^\s<>()\"]+", normalized_text)
    best_zoom_link = None

    for candidate in candidates:
        cleaned = normalize_meeting_link(candidate)
        platform = get_meeting_platform(cleaned)
        if not platform:
            continue

        if platform == "zoom":
            if "pwd=" in cleaned:
                return cleaned
            if not best_zoom_link:
                best_zoom_link = cleaned
            continue

        return cleaned

    return best_zoom_link


def normalize_meeting_link(link: str) -> str:
    cleaned = html.unescape(link).rstrip(".,);]")
    parsed = urlparse(cleaned)
    host = parsed.netloc.lower()
    if host.startswith("www."):
        host = host[4:]

    if host == "google.com" and parsed.path == "/url":
        query = parse_qs(parsed.query)
        redirect_target = (query.get("q") or query.get("url") or [None])[0]
        if redirect_target:
            return html.unescape(unquote(redirect_target)).rstrip(".,);]")

    return cleaned


def get_meeting_platform(link: str) -> str | None:
    host = urlparse(link).netloc.lower()
    if host.startswith("www."):
        host = host[4:]

    if host == "meet.google.com":
        return "google_meet"
    if host.endswith("zoom.us"):
        return "zoom"
    if host in {"teams.microsoft.com", "teams.live.com"} or host.endswith(".teams.microsoft.com"):
        return "microsoft_teams"
    return None


def format_join_time(value: datetime) -> str:
    return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


def parse_google_expiry(value: str | None) -> datetime | None:
    if not value:
        return None
    try:
        return datetime.fromtimestamp(int(value) / 1000, tz=timezone.utc)
    except (TypeError, ValueError):
        return None


def validate_google_webhook_url(webhook_url: str) -> None:
    parsed = urlparse(webhook_url)
    if parsed.scheme != "https":
        raise ValueError("Google Calendar webhook URL must use https")
    if not parsed.netloc:
        raise ValueError("Google Calendar webhook URL must be a public URL")
    if not parsed.path.endswith("/webhook/google-calendar"):
        raise ValueError("Google Calendar webhook URL must end with /webhook/google-calendar")
