"""Calendar tool: interact with Google Calendar."""

import json
import os
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

from google.auth.exceptions import RefreshError
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
from googleapiclient.errors import HttpError
from loguru import logger

from nanobot.agent.tools.base import Tool

# Scopes required for Google Calendar API
SCOPES = ["https://www.googleapis.com/auth/calendar"]

# Appended to failures so the model does not invent meetings when the API/auth did not succeed.
_NO_CALENDAR_DATA_FOR_MODEL = (
    "\n\n[NO_CALENDAR_DATA] Google Calendar was not read successfully. "
    "You MUST NOT infer, guess, or invent meetings, times, locations, or titles. "
    "Tell the user only that calendar could not be accessed and what they should fix "
    "(e.g. credentials_file, OAuth / calendar_token.json)."
)


class CalendarTool(Tool):
    """Tool to interact with Google Calendar."""

    name = "calendar"
    description = (
        "Interact with Google Calendar. REQUIRED: Always include 'action' parameter.\n\n"
        "Actions:\n"
        "- list_events: List upcoming events with details (title, time, ID). ALWAYS call this FIRST before update_event or delete_event.\n"
        "- create_event: Create new event. Requires title and start_time.\n"
        "- update_event: Update/reschedule an event. Requires event_id (get from list_events) and start_time (new time).\n"
        "- delete_event: Delete a single event. Requires event_id (get from list_events).\n"
        "- delete_events: Delete multiple events. Requires event_ids array (get from list_events).\n"
        "- check_availability: Check if a time slot is available.\n\n"
        "CRITICAL WORKFLOW for deletion (cancel/delete meetings):\n"
        "DO NOT EXPLAIN - EXECUTE IMMEDIATELY:\n"
        "1. IMMEDIATELY call calendar(action='list_events', time_min='today') - DO NOT explain, just execute\n"
        "2. Extract ALL event IDs from the response (long strings after '[ID: ' or in 'Event IDs:' line)\n"
        "3. IMMEDIATELY call calendar(action='delete_events', event_ids=[...]) with the extracted IDs\n"
        "NEVER use placeholder values like 'ID1', '[get event ID...]', or 'list_events'.\n"
        "NEVER explain what you will do - just execute the tools immediately.\n"
        "When you call this tool, the system will execute it automatically. Do not show JSON in your response - just call the tool.\n\n"
        "If the tool result contains [NO_CALENDAR_DATA] or starts with 'Error:' and describes auth/API failure, "
        "you have zero reliable calendar information—report the problem only; never make up events."
    )

    def __init__(self, calendar_config: Any = None):
        """
        Initialize calendar tool with calendar configuration.

        Args:
            calendar_config: Optional CalendarConfig instance. If None, loads from config.
        """
        self._calendar_config = calendar_config
        self._service = None
        self._calendar_refresh_rejected = False

    @property
    def config(self) -> Any:
        """Lazy load calendar config if not provided."""
        if self._calendar_config is None:
            from nanobot.config.loader import load_config

            config = load_config()
            self._calendar_config = config.tools.calendar
        return self._calendar_config
    
    def _get_timezone(self):
        """Get configured timezone or default to UTC."""
        import pytz
        config = self.config
        tz_str = getattr(config, 'timezone', 'UTC') or 'UTC'
        try:
            return pytz.timezone(tz_str)
        except Exception:
            return pytz.UTC

    def _get_credentials(self) -> Credentials | None:
        """Get valid user credentials from storage or OAuth flow."""
        self._calendar_refresh_rejected = False
        config = self.config

        if not config.enabled:
            return None

        if not config.credentials_file:
            return None

        creds_path = Path(config.credentials_file).expanduser()
        if not creds_path.exists():
            return None

        # Determine token file path
        if config.token_file:
            token_path = Path(config.token_file).expanduser()
        else:
            token_path = Path.home() / ".nanobot" / "calendar_token.json"
            token_path.parent.mkdir(parents=True, exist_ok=True)

        creds = None

        # Load existing token if available
        if token_path.exists():
            try:
                creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)
            except Exception:
                pass

        # If there are no (valid) credentials available, let the user log in
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                try:
                    creds.refresh(Request())
                except RefreshError as e:
                    self._calendar_refresh_rejected = True
                    logger.warning("Google Calendar OAuth refresh failed: {}", e)
                    creds = None
                except Exception:
                    creds = None

            if not creds:
                try:
                    with open(creds_path, "r") as f:
                        client_config = json.load(f)
                    flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
                    creds = flow.run_local_server(port=0)
                except Exception:
                    return None

            # Save the credentials for the next run
            try:
                with open(token_path, "w") as f:
                    f.write(creds.to_json())
            except Exception:
                pass

        return creds

    def _get_service(self):
        """Get Google Calendar service instance."""
        if self._service is None:
            creds = self._get_credentials()
            if not creds:
                return None
            self._service = build("calendar", "v3", credentials=creds)
        return self._service

    def coerce_params(self, params: dict[str, Any]) -> dict[str, Any]:
        """Coerce parameters, handling common name mismatches."""
        # Handle nested "parameters" key (some LLMs wrap params this way)
        if "parameters" in params and isinstance(params["parameters"], dict):
            # Extract nested parameters and merge with top-level
            nested = params.pop("parameters")
            params = {**params, **nested}
        
        # Remove common non-parameter keys that LLMs sometimes include
        params.pop("function", None)
        params.pop("functionName", None)
        params.pop("function_name", None)
        params.pop("tz", None)  # Not a valid parameter
        
        coerced = super().coerce_params(params)
        
        # Handle case where action is passed as a string argument instead of named parameter
        # e.g., calendar("calendar") or calendar("list_events")
        if "action" not in coerced and len(coerced) == 1:
            # Check if there's a single string value that could be the action
            for key, value in coerced.items():
                if isinstance(value, str) and value in ["list_events", "create_event", "delete_event", "delete_events", "update_event", "check_availability", "calendar"]:
                    coerced["action"] = "list_events" if value == "calendar" else value
                    coerced.pop(key, None)
                    break

        # Models often omit `action` and pass only a range ("this week", "1 week") under a junk key.
        if not coerced.get("action"):
            _known_actions = {
                "list_events",
                "create_event",
                "delete_event",
                "delete_events",
                "update_event",
                "check_availability",
                "calendar",
            }
            _create_keys = {"title", "start_time", "end_time", "event_id", "event_ids", "description", "location", "attendees"}
            if len(coerced) == 1:
                only_k, only_v = next(iter(coerced.items()))
                if only_k == "time_min" and isinstance(only_v, str):
                    coerced["action"] = "list_events"
                elif isinstance(only_v, str) and only_k not in _create_keys:
                    if only_v in _known_actions:
                        coerced["action"] = "list_events" if only_v == "calendar" else only_v
                        coerced.pop(only_k, None)
                    else:
                        coerced = {"action": "list_events", "time_min": only_v}
                else:
                    coerced["action"] = "list_events"
            else:
                coerced = {**coerced, "action": "list_events"}

        return coerced

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list_events", "create_event", "delete_event", "delete_events", "update_event", "check_availability"],
                    "description": "Action to perform: list_events (list upcoming events with details), create_event (create a new event), delete_event (delete a single event by ID), delete_events (delete multiple events by IDs), update_event (update/reschedule an event - requires event_id), check_availability (check if time slot is available)",
                },
                "event_id": {
                    "type": "string",
                    "description": "Event ID (required for delete_event). Get event IDs by calling list_events first.",
                },
                "event_ids": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of event IDs (required for delete_events). Get event IDs by calling list_events first.",
                },
                "title": {
                    "type": "string",
                    "description": "Event title/summary (required for create_event)",
                },
                "start_time": {
                    "type": "string",
                    "description": (
                        "Event start time. REQUIRED for create_event. "
                        "Use natural language formats like 'March 7 15:00', 'tomorrow 2pm', 'today at 5pm', "
                        "or relative formats like 'in 1 hour'. "
                        "DO NOT use ISO format - the tool will parse natural language and handle the current year automatically. "
                        "Examples: 'March 7 15:00', 'tomorrow at 2pm', 'today 18:00'"
                    ),
                },
                "end_time": {
                    "type": "string",
                    "description": "Event end time in ISO format (YYYY-MM-DDTHH:MM:SS) or relative. If not provided, defaults to 1 hour after start_time.",
                },
                "description": {
                    "type": "string",
                    "description": "Event description/details",
                },
                "location": {
                    "type": "string",
                    "description": "Event location",
                },
                "attendees": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of attendee email addresses",
                },
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of events to return (for list_events, default: 10)",
                    "default": 10,
                },
                "time_min": {
                    "type": "string",
                    "description": "Lower bound (exclusive) for an event's end time (ISO format) for list_events. Defaults to now.",
                },
            },
            "required": ["action"],
        }

    def _parse_time(self, time_str: str) -> datetime:
        """Parse time string (ISO format or relative like 'tomorrow 2pm').
        
        Returns timezone-aware datetime in the configured timezone (or UTC if not configured).
        """
        import re
        from datetime import timezone
        import pytz
        
        # Get configured timezone or default to UTC
        config = self.config
        tz_str = getattr(config, 'timezone', 'UTC') or 'UTC'
        try:
            tz = pytz.timezone(tz_str)
        except Exception:
            # Invalid timezone, fall back to UTC
            tz = pytz.UTC
        
        original_str = time_str
        time_str = time_str.strip().lower()

        # Try ISO format first, but validate year is reasonable
        now = datetime.now(tz)
        try:
            dt = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
            # Ensure timezone-aware - if no timezone, assume configured timezone
            if dt.tzinfo is None:
                dt = tz.localize(dt)
            
            # Validate year is current year or future (prevent scheduling in past years)
            current_year = now.year
            if dt.year < current_year:
                # If year is in the past, assume user meant current year
                dt = dt.replace(year=current_year)
                # If still in the past, use next year
                if dt < now:
                    dt = dt.replace(year=current_year + 1)
            
            return dt
        except ValueError:
            pass

        # Try parsing month names (e.g., "March 7 15:00", "March 7th at 3pm")
        month_names = {
            "january": 1, "jan": 1, "february": 2, "feb": 2, "march": 3, "mar": 3,
            "april": 4, "apr": 4, "may": 5, "june": 6, "jun": 6, "july": 7, "jul": 7,
            "august": 8, "aug": 8, "september": 9, "sep": 9, "sept": 9,
            "october": 10, "oct": 10, "november": 11, "nov": 11, "december": 12, "dec": 12,
        }
        
        # Pattern: "March 7 15:00" or "March 7th at 3pm" or "on March 7 at 15:00"
        month_patterns = [
            r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?\s+(?:at\s+)?(\d{1,2}):(\d{2})",  # 24-hour
            r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?\s+(?:at\s+)?(\d{1,2})\s*(am|pm)",  # 12-hour
            r"(?:on\s+)?(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)\s+(\d{1,2})(?:st|nd|rd|th)?",  # Date only
        ]
        
        for pattern in month_patterns:
            match = re.search(pattern, time_str, re.IGNORECASE)
            if match:
                groups = match.groups()
                month_name = groups[0].lower()
                if month_name in month_names:
                    month = month_names[month_name]
                    day = int(groups[1])
                    year = now.year
                    
                    # Check if date is in the past
                    test_date = tz.localize(datetime(year, month, day))
                    if test_date < now.replace(hour=0, minute=0, second=0, microsecond=0):
                        year += 1
                    
                    if len(groups) >= 4 and groups[2] and groups[3]:
                        if groups[3].lower() in ['am', 'pm']:
                            # 12-hour format
                            hour = int(groups[2])
                            period = groups[3].lower()
                            minute = 0
                            if period == "pm" and hour != 12:
                                hour += 12
                            elif period == "am" and hour == 12:
                                hour = 0
                        else:
                            # 24-hour format
                            hour = int(groups[2])
                            minute = int(groups[3])
                        result = tz.localize(datetime(year, month, day, hour, minute))
                        return result
                    else:
                        # Date only, default to 9am
                        result = tz.localize(datetime(year, month, day, 9, 0))
                        return result

        # Parse relative times
        now = datetime.now(tz)
        if time_str.startswith("in "):
            # "in 1 hour", "in 30 minutes"
            parts = time_str[3:].split()
            if len(parts) >= 2:
                try:
                    amount = int(parts[0])
                    unit = parts[1]
                    if "hour" in unit:
                        return now + timedelta(hours=amount)
                    elif "minute" in unit:
                        return now + timedelta(minutes=amount)
                    elif "day" in unit:
                        return now + timedelta(days=amount)
                except ValueError:
                    pass

        # Handle "today" - same logic as "tomorrow" but use today's date
        if "today" in time_str:
            base = now.replace(hour=0, minute=0, second=0, microsecond=0)
            # Try to extract time - same patterns as tomorrow
            hour = None
            minute = 0
            
            # Pattern 1: "2:00pm" or "2:00 pm" (with minutes and am/pm)
            match = re.search(r"(\d{1,2})\s*:\s*(\d{2})\s*(am|pm)", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                minute = int(match.group(2))
                period = match.group(3).lower()
                if period == "pm" and hour != 12:
                    hour += 12
                elif period == "am" and hour == 12:
                    hour = 0
                result = base.replace(hour=hour, minute=minute, second=0, microsecond=0)
                # If time has passed today, it's invalid - return error or use tomorrow?
                if result < now:
                    # Time has passed, schedule for tomorrow instead
                    result = result + timedelta(days=1)
                return result
            
            # Pattern 2: "2pm" or "2 pm" (hour only with am/pm)
            match = re.search(r"(\d{1,2})\s*(am|pm)\b", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                period = match.group(2).lower()
                if period == "pm" and hour != 12:
                    hour += 12
                elif period == "am" and hour == 12:
                    hour = 0
                result = base.replace(hour=hour, minute=0, second=0, microsecond=0)
                # If time has passed today, schedule for tomorrow
                if result < now:
                    result = result + timedelta(days=1)
                return result
            
            # Pattern 3: "17:00" or "today 17:00" (24-hour format with colon, no am/pm)
            match = re.search(r"(\d{1,2})\s*:\s*(\d{2})(?!\s*(am|pm))", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                minute = int(match.group(2))
                # Validate hour is in 24-hour range
                if 0 <= hour <= 23 and 0 <= minute <= 59:
                    result = base.replace(hour=hour, minute=minute, second=0, microsecond=0)
                    # If time has passed today, schedule for tomorrow
                    if result < now:
                        result = result + timedelta(days=1)
                    return result
            
            # Pattern 4: "at 17:00" (24-hour format without am/pm, with "at")
            match = re.search(r"at\s+(\d{1,2})\s*:\s*(\d{2})", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                minute = int(match.group(2))
                if 0 <= hour <= 23 and 0 <= minute <= 59:
                    result = base.replace(hour=hour, minute=minute, second=0, microsecond=0)
                    if result < now:
                        result = result + timedelta(days=1)
                    return result
            
            # Pattern 5: "at 5" (hour only, assume 24-hour if > 12, else assume pm)
            match = re.search(r"at\s+(\d{1,2})(?!\s*(am|pm))", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                # If hour is 1-12, assume PM unless it's clearly morning context
                if hour <= 12 and hour > 0:
                    hour += 12  # Default to PM for afternoon times
                result = base.replace(hour=hour, minute=0, second=0, microsecond=0)
                if result < now:
                    result = result + timedelta(days=1)
                return result
            
            # Default to 9am today (or tomorrow if 9am has passed)
            result = base.replace(hour=9, minute=0, second=0, microsecond=0)
            if result < now:
                result = result + timedelta(days=1)
            return result

        if "tomorrow" in time_str:
            base = now + timedelta(days=1)
            # Try to extract time - improved regex to handle various formats
            # Try patterns in order of specificity (most specific first)
            hour = None
            minute = 0
            
            # Pattern 1: "2:00pm" or "2:00 pm" (with minutes and am/pm)
            match = re.search(r"(\d{1,2})\s*:\s*(\d{2})\s*(am|pm)", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                minute = int(match.group(2))
                period = match.group(3).lower()
                if period == "pm" and hour != 12:
                    hour += 12
                elif period == "am" and hour == 12:
                    hour = 0
                result = base.replace(hour=hour, minute=minute, second=0, microsecond=0)
                return result
            
            # Pattern 2: "2pm" or "2 pm" (hour only with am/pm) - must check this before 24-hour patterns
            # to avoid matching "14" in "14:00" as "14pm"
            match = re.search(r"(\d{1,2})\s*(am|pm)\b", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                period = match.group(2).lower()
                if period == "pm" and hour != 12:
                    hour += 12
                elif period == "am" and hour == 12:
                    hour = 0
                result = base.replace(hour=hour, minute=0, second=0, microsecond=0)
                return result
            
            # Pattern 3: "14:00" or "tomorrow 14:00" (24-hour format with colon, no am/pm)
            # This handles cases where the agent converts "2pm" to "14:00"
            match = re.search(r"(\d{1,2})\s*:\s*(\d{2})(?!\s*(am|pm))", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                minute = int(match.group(2))
                # Validate hour is in 24-hour range
                if 0 <= hour <= 23 and 0 <= minute <= 59:
                    result = base.replace(hour=hour, minute=minute, second=0, microsecond=0)
                    return result
            
            # Pattern 4: "at 2:00" (24-hour format without am/pm, with "at")
            match = re.search(r"at\s+(\d{1,2})\s*:\s*(\d{2})", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                minute = int(match.group(2))
                if 0 <= hour <= 23 and 0 <= minute <= 59:
                    result = base.replace(hour=hour, minute=minute, second=0, microsecond=0)
                    return result
            
            # Pattern 5: "at 2" (hour only, assume 24-hour if > 12, else assume pm)
            match = re.search(r"at\s+(\d{1,2})(?!\s*(am|pm))", time_str, re.IGNORECASE)
            if match:
                hour = int(match.group(1))
                # If hour is 1-12, assume PM unless it's clearly morning context
                if hour <= 12 and hour > 0:
                    hour += 12  # Default to PM for afternoon times
                result = base.replace(hour=hour, minute=0, second=0, microsecond=0)
                return result
            
            # Default to 9am if no time specified
            result = base.replace(hour=9, minute=0, second=0, microsecond=0)
            return result

        # Default: assume it's today at the specified time or now
        return now

    def _parse_time_with_date(self, time_str: str, base_date: Any) -> datetime:
        """Parse a time string (like '4pm') in the context of a specific date.
        
        This is used when updating events - if user says 'move to 4pm', 
        we want to use the original event's date, not 'today' relative to now.
        """
        import re
        from datetime import date
        
        tz = self._get_timezone()
        # Create a datetime for the base date at midnight in the configured timezone
        base_dt = tz.localize(datetime.combine(base_date, datetime.min.time()))
        
        time_str_lower = time_str.strip().lower()
        
        # Pattern 1: "4pm" or "4 pm" (hour only with am/pm)
        match = re.search(r"(\d{1,2})\s*(am|pm)\b", time_str_lower, re.IGNORECASE)
        if match:
            hour = int(match.group(1))
            period = match.group(2).lower()
            if period == "pm" and hour != 12:
                hour += 12
            elif period == "am" and hour == 12:
                hour = 0
            return base_dt.replace(hour=hour, minute=0, second=0, microsecond=0)
        
        # Pattern 2: "4:00pm" or "4:00 pm" (with minutes and am/pm)
        match = re.search(r"(\d{1,2})\s*:\s*(\d{2})\s*(am|pm)", time_str_lower, re.IGNORECASE)
        if match:
            hour = int(match.group(1))
            minute = int(match.group(2))
            period = match.group(3).lower()
            if period == "pm" and hour != 12:
                hour += 12
            elif period == "am" and hour == 12:
                hour = 0
            return base_dt.replace(hour=hour, minute=minute, second=0, microsecond=0)
        
        # Pattern 3: "16:00" (24-hour format)
        match = re.search(r"(\d{1,2})\s*:\s*(\d{2})(?!\s*(am|pm))", time_str_lower, re.IGNORECASE)
        if match:
            hour = int(match.group(1))
            minute = int(match.group(2))
            if 0 <= hour <= 23 and 0 <= minute <= 59:
                return base_dt.replace(hour=hour, minute=minute, second=0, microsecond=0)
        
        # If no simple time pattern matches, fall back to full _parse_time
        # but adjust the date to match base_date
        parsed = self._parse_time(time_str)
        if parsed.date() != base_date:
            # Adjust to use the base_date, keeping the time
            return parsed.replace(year=base_date.year, month=base_date.month, day=base_date.day)
        return parsed

    async def execute(
        self,
        action: str,
        title: str | None = None,
        start_time: str | None = None,
        end_time: str | None = None,
        description: str | None = None,
        location: str | None = None,
        attendees: list[str] | None = None,
        max_results: int = 10,
        time_min: str | None = None,
        event_id: str | None = None,
        event_ids: list[str] | None = None,
        **kwargs: Any,
    ) -> str:
        """
        Execute calendar operation.

        Args:
            action: Action to perform (list_events, create_event, check_availability)
            title: Event title (for create_event)
            start_time: Start time in ISO format or relative (for create_event)
            end_time: End time in ISO format or relative (for create_event)
            description: Event description
            location: Event location
            attendees: List of attendee email addresses
            max_results: Maximum number of events to return (for list_events)
            time_min: Lower bound for event end time (for list_events)
            **kwargs: Ignore extra parameters

        Returns:
            Result string
        """
        config = self.config

        if not config.enabled:
            return (
                "Error: Calendar is not enabled. Set NANOBOT_TOOLS__CALENDAR__ENABLED=true"
                + _NO_CALENDAR_DATA_FOR_MODEL
            )

        service = self._get_service()
        if not service:
            msg = (
                "Error: Could not authenticate with Google Calendar. "
                "Please ensure credentials_file is configured and valid. "
                "You may need to run OAuth flow once to authorize access."
            )
            if self._calendar_refresh_rejected:
                msg += (
                    " Google rejected the saved refresh token (expired or revoked). "
                    "Re-authorize on a machine with a browser and replace ~/.nanobot/calendar_token.json on the host, "
                    "then restart the container. If the token file is mounted :ro, refreshes cannot be persisted—"
                    "use rw or re-auth on the host when that happens."
                )
            return msg + _NO_CALENDAR_DATA_FOR_MODEL

        calendar_id = config.calendar_id or "primary"

        try:
            if action == "list_events":
                return await self._list_events(service, calendar_id, max_results, time_min)
            elif action == "create_event":
                if not title:
                    return "Error: title is required for create_event"
                if not start_time:
                    return "Error: start_time is required for create_event"
                return await self._create_event(
                    service,
                    calendar_id,
                    title,
                    start_time,
                    end_time,
                    description,
                    location,
                    attendees,
                )
            elif action == "update_event":
                if not event_id:
                    return (
                        "ERROR: event_id is required for update_event. "
                        "YOU MUST call calendar(action='list_events', time_min='today') FIRST to get the actual event ID. "
                        "Do NOT use placeholder values."
                    )
                # Validate event_id is not a placeholder
                if any(placeholder in event_id.lower() for placeholder in [
                    "get event", "from calendar", "list_events", "<id", "placeholder", 
                    "[get", "event id", "id of", "id from", "[id from", "previous call"
                ]) or len(event_id) < 15:
                    return (
                        f"STOP: Invalid event_id '{event_id}' - this is a placeholder, not a real ID. "
                        "You MUST call calendar(action='list_events', time_min='today') NOW to get the actual event ID. "
                        "Do NOT explain - just call list_events immediately, then extract the ID from the response and call update_event again."
                    )
                # Check if user wants to cancel (via status parameter in kwargs)
                status = kwargs.get("status", "").lower() if kwargs else ""
                if status == "cancelled":
                    # To cancel a meeting, we delete it
                    return await self._delete_event(service, calendar_id, event_id)
                
                # For update_event, start_time and end_time are the new times
                return await self._update_event(
                    service,
                    calendar_id,
                    event_id,
                    title,
                    start_time,  # New start time
                    end_time,    # New end time
                    description,
                    location,
                    attendees,
                )
            elif action == "delete_event":
                if not event_id:
                    return (
                        "STOP: event_id is required for delete_event. "
                        "DO NOT EXPLAIN. IMMEDIATELY call calendar(action='list_events', time_min='today') NOW. "
                        "Then extract event IDs from the response and call delete_events (plural) with those IDs."
                    )
                # Validate event_id is not a placeholder
                if any(placeholder in event_id.lower() for placeholder in [
                    "get event", "from calendar", "list_events", "<id", "placeholder", 
                    "[get", "event id", "id of", "id from", "[id from", "previous call", "list events call"
                ]) or len(event_id) < 15:
                    return (
                        f"STOP: Invalid event_id '{event_id}' - this is a placeholder, not a real ID. "
                        f"DO NOT EXPLAIN. IMMEDIATELY call calendar(action='list_events', time_min='today') NOW. "
                        f"Then extract event IDs from the response and call delete_events (plural) with those IDs. "
                        f"Event IDs are long alphanumeric strings (20+ characters, no spaces) from the list_events response."
                    )
                return await self._delete_event(service, calendar_id, event_id)
            elif action == "delete_events":
                if not event_ids:
                    return (
                        "STOP: event_ids (array) is required for delete_events. "
                        "DO NOT EXPLAIN. IMMEDIATELY call calendar(action='list_events', time_min='today') NOW. "
                        "Then extract ALL event IDs from the response (they appear after '[ID: ' or in the 'Event IDs:' line) "
                        "and call delete_events with those IDs."
                    )
                return await self._delete_events(service, calendar_id, event_ids)
            elif action == "check_availability":
                if not start_time:
                    return "Error: start_time is required for check_availability"
                return await self._check_availability(service, calendar_id, start_time, end_time)
            else:
                return f"Error: Unknown action '{action}'. Use 'list_events', 'create_event', 'delete_event', 'delete_events', 'update_event', or 'check_availability'"
        except HttpError as e:
            return f"Error accessing Google Calendar API: {e}" + _NO_CALENDAR_DATA_FOR_MODEL
        except Exception as e:
            return f"Error: {str(e)}" + _NO_CALENDAR_DATA_FOR_MODEL

    async def _list_events(
        self, service: Any, calendar_id: str, max_results: int, time_min: str | None
    ) -> str:
        """List upcoming events."""
        import asyncio

        def _list():
            now = datetime.utcnow().isoformat() + "Z"
            # Handle "now", "today", or None - convert to ISO format
            if not time_min or time_min.lower() in ["now", "today"]:
                time_min_str = now
            elif time_min.endswith("Z") or "+" in time_min or "-" in time_min[-6:]:
                # Already in ISO format
                time_min_str = time_min
            else:
                # Try to parse as natural language and convert to ISO
                try:
                    parsed_dt = self._parse_time(time_min)
                    time_min_str = parsed_dt.isoformat()
                except Exception:
                    # If parsing fails, use current time
                    time_min_str = now

            events_result = (
                service.events()
                .list(
                    calendarId=calendar_id,
                    timeMin=time_min_str,
                    maxResults=max_results,
                    singleEvents=True,
                    orderBy="startTime",
                    # Request additional fields to get attendees and attachments
                    fields="items(id,summary,start,end,description,location,attendees(email,responseStatus),attachments(fileUrl,title))",
                )
                .execute()
            )

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

            if not events:
                return "No upcoming events found."

            result = [f"Found {len(events)} upcoming event(s):\n"]
            event_ids = []
            for idx, event in enumerate(events, 1):
                start = event["start"].get("dateTime", event["start"].get("date"))
                end = event["end"].get("dateTime", event["end"].get("date"))
                title = event.get("summary", "No title")
                event_id = event.get("id", "")
                event_ids.append(event_id)
                
                # Extract additional information
                description = event.get("description", "")
                location = event.get("location", "")
                attendees = event.get("attendees", [])
                attachments = event.get("attachments", [])
                
                # Add position indicator - "last" means the one with the latest time (usually the last in list)
                position_note = ""
                if idx == len(events):
                    position_note = " (LAST - latest time)"
                elif idx == 1:
                    position_note = " (FIRST - earliest time)"
                
                # Format: include numbered list with position indicators
                result.append(f"{idx}. {title} ({start} - {end}) [ID: {event_id}]{position_note}")
                
                # Add location if present
                if location:
                    result.append(f"   Location: {location}")
                
                # Add attendees if present
                if attendees:
                    attendee_emails = [att.get("email", "") for att in attendees if att.get("email")]
                    if attendee_emails:
                        result.append(f"   Attendees: {', '.join(attendee_emails)}")
                
                # Add attachments if present
                if attachments:
                    attachment_info = []
                    for att in attachments:
                        file_name = att.get("fileUrl", "").split("/")[-1] if att.get("fileUrl") else att.get("title", "Unknown file")
                        attachment_info.append(file_name)
                    if attachment_info:
                        result.append(f"   Attachments: {', '.join(attachment_info)}")
                
                # Add description if present (truncate if too long)
                if description:
                    desc_preview = description[:100] + "..." if len(description) > 100 else description
                    result.append(f"   Description: {desc_preview}")

            # Also include a summary line with all IDs for easy extraction
            result.append(f"\nEvent IDs: {', '.join(event_ids)}")
            result.append(f"\nNote: 'last meeting' means the meeting with the latest time (usually #{len(events)} in the list above).")
            
            return "\n".join(result)

        return await asyncio.to_thread(_list)

    async def _create_event(
        self,
        service: Any,
        calendar_id: str,
        title: str,
        start_time: str,
        end_time: str | None,
        description: str | None,
        location: str | None,
        attendees: list[str] | None,
    ) -> str:
        """Create a new calendar event."""
        import asyncio

        def _create():
            start_dt = self._parse_time(start_time)
            if end_time:
                end_dt = self._parse_time(end_time)
            else:
                end_dt = start_dt + timedelta(hours=1)  # Default 1 hour

            # Get configured timezone
            config = self.config
            tz_str = getattr(config, 'timezone', 'UTC') or 'UTC'
            
            # Validate that start time is not in the past
            tz = self._get_timezone()
            now = datetime.now(tz)
            if start_dt < now:
                raise ValueError(
                    f"Cannot schedule event in the past. Start time ({start_dt}) is before current time ({now}). "
                    f"Parsed from start_time='{start_time}'. "
                    f"Tip: Use natural language formats like 'March 7 15:00' or 'tomorrow 2pm' instead of ISO format."
                )

            # Validate time range
            if end_dt <= start_dt:
                raise ValueError(
                    f"Invalid time range: end time ({end_dt}) must be after start time ({start_dt}). "
                    f"Parsed from start_time='{start_time}', end_time='{end_time}'"
                )

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

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

            try:
                created_event = service.events().insert(calendarId=calendar_id, body=event).execute()
                return f"Event created: {created_event.get('htmlLink')}"
            except HttpError as e:
                error_details = e.error_details if hasattr(e, 'error_details') else str(e)
                error_msg = str(e)
                # Provide more detailed error information
                return (
                    f"Error creating calendar event: {error_msg}. "
                    f"Details: {error_details}. "
                    f"Parsed start_time='{start_time}' -> {start_dt.isoformat()}, "
                    f"end_time='{end_time}' -> {end_dt.isoformat()}"
                )
            except Exception as e:
                return (
                    f"Error creating calendar event: {str(e)}. "
                    f"Parsed start_time='{start_time}' -> {start_dt.isoformat()}, "
                    f"end_time='{end_time}' -> {end_dt.isoformat()}"
                )

        return await asyncio.to_thread(_create)

    async def _check_availability(
        self, service: Any, calendar_id: str, start_time: str, end_time: str | None
    ) -> str:
        """Check if a time slot is available."""
        import asyncio

        def _check():
            start_dt = self._parse_time(start_time)
            if end_time:
                end_dt = self._parse_time(end_time)
            else:
                end_dt = start_dt + timedelta(hours=1)

            time_min_str = start_dt.isoformat() + "Z"
            time_max_str = end_dt.isoformat() + "Z"

            events_result = (
                service.events()
                .list(
                    calendarId=calendar_id,
                    timeMin=time_min_str,
                    timeMax=time_max_str,
                    singleEvents=True,
                    orderBy="startTime",
                )
                .execute()
            )

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

            if events:
                conflicts = [e.get("summary", "Untitled") for e in events]
                return f"Time slot is NOT available. Conflicts with: {', '.join(conflicts)}"
            else:
                return f"Time slot is available ({start_dt} to {end_dt})"

        return await asyncio.to_thread(_check)

    async def _delete_event(
        self, service: Any, calendar_id: str, event_id: str
    ) -> str:
        """Delete a calendar event by ID."""
        import asyncio

        def _delete():
            try:
                service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
                return f"Event deleted successfully (ID: {event_id})"
            except HttpError as e:
                if e.resp.status == 404:
                    return f"Error: Event not found (ID: {event_id}). It may have already been deleted."
                return f"Error deleting event: {e}"
            except Exception as e:
                return f"Error deleting event: {str(e)}"

        return await asyncio.to_thread(_delete)

    async def _delete_events(
        self, service: Any, calendar_id: str, event_ids: list[str]
    ) -> str:
        """Delete multiple calendar events by IDs."""
        import asyncio

        def _delete_all():
            # Filter out placeholder/invalid IDs
            valid_ids = []
            invalid_ids = []
            
            for event_id in event_ids:
                if not isinstance(event_id, str):
                    invalid_ids.append(str(event_id))
                    continue
                
                # Check for common invalid patterns:
                # 1. Placeholders like "ID1", "ID2", etc.
                # 2. Error messages or instructions (contain common words)
                # 3. Too short (real Google Calendar IDs are typically 20+ characters)
                # 4. Contains spaces (real IDs don't have spaces)
                invalid_patterns = [
                    event_id.upper() in ["ID1", "ID2", "ID3", "ID4", "ID5"],
                    len(event_id) < 15,  # Real Google Calendar IDs are longer
                    " " in event_id,  # Real IDs don't have spaces
                    any(word in event_id.lower() for word in [
                        "get", "event", "id", "by", "calling", "list", "first",
                        "extract", "from", "response", "error", "invalid"
                    ]),  # Likely an instruction/error message
                ]
                
                if any(invalid_patterns):
                    invalid_ids.append(event_id)
                else:
                    valid_ids.append(event_id)
            
            if invalid_ids:
                return (
                    f"ERROR: Invalid event IDs detected: {invalid_ids}. "
                    f"STOP and call calendar(action='list_events', time_min='today') FIRST to get actual event IDs. "
                    f"Do NOT call delete_events again until you have called list_events and extracted the real IDs. "
                    f"Event IDs are long alphanumeric strings (20+ characters, no spaces) from the list_events response."
                )
            
            if not valid_ids:
                return (
                    "ERROR: No valid event IDs provided. "
                    "YOU MUST call calendar(action='list_events', time_min='today') FIRST. "
                    "Do NOT call delete_events again. Call list_events now, extract the IDs from the response "
                    "(they appear after '[ID: ' or in the 'Event IDs:' line), then call delete_events with those IDs."
                )
            
            deleted = []
            failed = []
            
            for event_id in valid_ids:
                try:
                    service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
                    deleted.append(event_id)
                except HttpError as e:
                    if e.resp.status == 404:
                        # Already deleted, count as success
                        deleted.append(event_id)
                    else:
                        failed.append((event_id, str(e)))
                except Exception as e:
                    failed.append((event_id, str(e)))
            
            result_parts = []
            if deleted:
                result_parts.append(f"Successfully deleted {len(deleted)} event(s).")
            if failed:
                result_parts.append(f"Failed to delete {len(failed)} event(s): {failed}")
            
            return " ".join(result_parts) if result_parts else "No events to delete."

        return await asyncio.to_thread(_delete_all)

    async def _update_event(
        self,
        service: Any,
        calendar_id: str,
        event_id: str,
        title: str | None = None,
        start_time: str | None = None,
        end_time: str | None = None,
        description: str | None = None,
        location: str | None = None,
        attendees: list[str] | None = None,
    ) -> str:
        """Update/reschedule a calendar event."""
        import asyncio

        def _update():
            try:
                # Get existing event
                event = service.events().get(calendarId=calendar_id, eventId=event_id).execute()
                
                # Save ORIGINAL start/end times BEFORE we modify them (needed for duration calculation)
                original_start_raw = event["start"].get("dateTime", event["start"].get("date"))
                original_end_raw = event["end"].get("dateTime", event["end"].get("date"))
                
                # Get original event date for context
                try:
                    if "T" in original_start_raw:
                        original_start_dt = datetime.fromisoformat(original_start_raw.replace("Z", "+00:00"))
                        original_date = original_start_dt.date()
                        old_start_dt = original_start_dt
                    else:
                        original_date = datetime.fromisoformat(original_start_raw).date()
                        tz = self._get_timezone()
                        old_start_dt = tz.localize(datetime.combine(original_date, datetime.min.time()))
                except Exception:
                    # Fallback to today if parsing fails
                    tz = self._get_timezone()
                    original_date = datetime.now(tz).date()
                    old_start_dt = datetime.now(tz)
                
                try:
                    if "T" in original_end_raw:
                        old_end_dt = datetime.fromisoformat(original_end_raw.replace("Z", "+00:00"))
                    else:
                        tz = self._get_timezone()
                        old_end_dt = tz.localize(datetime.combine(original_date, datetime.min.time()))
                except Exception:
                    old_end_dt = old_start_dt + timedelta(hours=1)  # Default 1 hour duration
                
                # Update fields if provided
                if title:
                    event["summary"] = title
                if start_time:
                    # Parse start_time in context of the original event's date
                    # If it's just a time like "4pm", use the original event's date
                    start_dt = self._parse_time_with_date(start_time, original_date)
                    config = self.config
                    tz_str = getattr(config, 'timezone', 'UTC') or 'UTC'
                    event["start"] = {
                        "dateTime": start_dt.isoformat(),
                        "timeZone": tz_str,
                    }
                if end_time:
                    # Parse end_time in context of the original event's date (or new start date if start_time was provided)
                    base_date = original_date
                    if start_time and "start" in event:
                        try:
                            new_start_raw = event["start"].get("dateTime", "")
                            if "T" in new_start_raw:
                                base_date = datetime.fromisoformat(new_start_raw.replace("Z", "+00:00")).date()
                        except Exception:
                            pass
                    end_dt = self._parse_time_with_date(end_time, base_date)
                    config = self.config
                    tz_str = getattr(config, 'timezone', 'UTC') or 'UTC'
                    event["end"] = {
                        "dateTime": end_dt.isoformat(),
                        "timeZone": tz_str,
                    }
                elif start_time and "start" in event:
                    # If start_time changed but end_time not provided, adjust end_time to maintain duration
                    # Use the ORIGINAL start/end times we saved above
                    duration = old_end_dt - old_start_dt
                    
                    # Get the NEW start time (already updated above)
                    new_start_dt = datetime.fromisoformat(event["start"].get("dateTime", "").replace("Z", "+00:00"))
                    new_end_dt = new_start_dt + duration
                    config = self.config
                    tz_str = getattr(config, 'timezone', 'UTC') or 'UTC'
                    event["end"] = {
                        "dateTime": new_end_dt.isoformat(),
                        "timeZone": tz_str,
                    }
                
                if description is not None:
                    event["description"] = description
                if location is not None:
                    event["location"] = location
                if attendees is not None:
                    event["attendees"] = [{"email": email} for email in attendees]

                # Update the event
                updated_event = service.events().update(
                    calendarId=calendar_id,
                    eventId=event_id,
                    body=event
                ).execute()
                
                return f"Event updated successfully: {updated_event.get('htmlLink')}"
            except HttpError as e:
                if e.resp.status == 404:
                    return f"Error: Event not found (ID: {event_id}). It may have been deleted."
                return f"Error updating event: {e}"
            except Exception as e:
                return f"Error updating event: {str(e)}"

        return await asyncio.to_thread(_update)

