"""
Built-in tool: calendar_lookup

Reads and creates Google Calendar events using per-tenant OAuth tokens.

Authentication:
  Tokens are stored per-tenant in channel_connections (channel_type='google_calendar').
  The tenant admin must complete the OAuth flow via /api/portal/calendar/install first.
  If no token is found, returns an informative message asking admin to connect.

Actions:
  - list:               List events for the given date (default)
  - check_availability: Return free/busy summary for the given date
  - create:             Create a new calendar event

Token auto-refresh:
  google.oauth2.credentials.Credentials auto-refreshes expired access tokens
  using the stored refresh_token. After each API call, if credentials.token
  changed (refresh occurred), the updated token is encrypted and written back
  to channel_connections so subsequent calls don't re-trigger refresh.

All results are formatted as natural language strings — no raw JSON exposed.
"""

from __future__ import annotations

import asyncio
import json
import logging
import uuid
from typing import Any

# Module-level imports for patchability in tests.
# google-auth and googleapiclient are optional dependencies — import errors handled
# gracefully in the functions that use them.
try:
    from googleapiclient.discovery import build  # type: ignore[import-untyped]
except ImportError:
    build = None  # type: ignore[assignment]

from shared.config import settings
from shared.crypto import KeyEncryptionService

logger = logging.getLogger(__name__)

# Google Calendar API scope (must match what was requested during OAuth)
_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar"
_GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"


def google_credentials_from_token(token_dict: dict[str, Any]) -> Any:
    """
    Build a google.oauth2.credentials.Credentials object from a stored token dict.

    The token dict is the JSON structure written by calendar_auth.py during OAuth:
      {
        "token": "ya29.access_token",
        "refresh_token": "1//refresh_token",
        "token_uri": "https://oauth2.googleapis.com/token",
        "client_id": "...",
        "client_secret": "...",
        "scopes": ["https://www.googleapis.com/auth/calendar"]
      }

    Args:
        token_dict: Parsed token dictionary.

    Returns:
        google.oauth2.credentials.Credentials instance.

    Raises:
        ImportError: If google-auth is not installed.
    """
    from google.oauth2.credentials import Credentials  # type: ignore[import-untyped]

    return Credentials(
        token=token_dict.get("token"),
        refresh_token=token_dict.get("refresh_token"),
        token_uri=token_dict.get("token_uri", _GOOGLE_TOKEN_URL),
        client_id=token_dict.get("client_id"),
        client_secret=token_dict.get("client_secret"),
        scopes=token_dict.get("scopes", [_CALENDAR_SCOPE]),
    )


async def calendar_lookup(
    date: str,
    action: str = "list",
    event_summary: str | None = None,
    event_start: str | None = None,
    event_end: str | None = None,
    calendar_id: str = "primary",
    tenant_id: str | None = None,
    _session: Any = None,  # Injected in tests; production uses DB session from task context
    **kwargs: object,
) -> str:
    """
    Look up, check availability, or create Google Calendar events for a specific date.

    Args:
        date:           Date in YYYY-MM-DD format (required).
        action:         One of "list", "check_availability", "create". Default: "list".
        event_summary:  Event title (required for action="create").
        event_start:    ISO 8601 datetime with timezone (required for action="create").
        event_end:      ISO 8601 datetime with timezone (required for action="create").
        calendar_id:    Google Calendar ID. Defaults to 'primary'.
        tenant_id:      Konstruct tenant UUID string. Required for token lookup.
        _session:       Injected AsyncSession (for testing). Production passes None.

    Returns:
        Natural language string describing the result.
    """
    # Guard: tenant_id is required to look up per-tenant OAuth token
    if not tenant_id:
        return "Calendar not available: missing tenant context."

    # Get DB session
    session = _session
    if session is None:
        # Production: obtain a session from the DB pool
        # Import here to avoid circular imports at module load time
        try:
            from shared.db import async_session_factory
            session = async_session_factory()
            # Note: caller is responsible for closing the session
            # In practice, the orchestrator task context manages session lifecycle
        except Exception:
            logger.exception("Failed to create DB session for calendar_lookup")
            return "Calendar lookup failed: unable to connect to the database."

    try:
        tenant_uuid = uuid.UUID(tenant_id)
    except ValueError:
        return f"Calendar lookup failed: invalid tenant ID '{tenant_id}'."

    # Load per-tenant OAuth token from channel_connections
    try:
        from sqlalchemy import select
        from shared.models.tenant import ChannelConnection, ChannelTypeEnum

        result = await session.execute(
            select(ChannelConnection).where(
                ChannelConnection.tenant_id == tenant_uuid,
                ChannelConnection.channel_type == ChannelTypeEnum.GOOGLE_CALENDAR,
            )
        )
        conn = result.scalar_one_or_none()
    except Exception:
        logger.exception("DB error loading calendar connection for tenant=%s", tenant_id)
        return "Calendar lookup failed: database error loading calendar connection."

    if conn is None:
        return (
            "Google Calendar is not connected for this tenant. "
            "Ask an admin to connect it in Settings."
        )

    # Decrypt token
    encrypted_token = conn.config.get("token", "")
    if not encrypted_token:
        return "Calendar lookup failed: no token found in connection config."

    try:
        if not settings.platform_encryption_key:
            return "Calendar lookup failed: encryption key not configured."

        enc_svc = KeyEncryptionService(
            primary_key=settings.platform_encryption_key,
            previous_key=settings.platform_encryption_key_previous,
        )
        token_json: str = enc_svc.decrypt(encrypted_token)
        token_dict: dict[str, Any] = json.loads(token_json)
    except Exception:
        logger.exception("Failed to decrypt calendar token for tenant=%s", tenant_id)
        return "Calendar lookup failed: unable to decrypt stored credentials."

    # Build Google credentials
    try:
        creds = google_credentials_from_token(token_dict)
    except ImportError:
        return (
            "Google Calendar library not installed. "
            "Run: uv add google-api-python-client google-auth"
        )
    except Exception:
        logger.exception("Failed to build Google credentials for tenant=%s", tenant_id)
        return "Calendar lookup failed: invalid stored credentials."

    # Record the token before the API call to detect refresh
    token_before = creds.token

    # Execute the API call in a thread executor (blocking SDK)
    try:
        result_str = await asyncio.get_event_loop().run_in_executor(
            None,
            _execute_calendar_action,
            creds,
            action,
            date,
            calendar_id,
            event_summary,
            event_start,
            event_end,
        )
    except Exception:
        logger.exception("Calendar API call failed for tenant=%s date=%s action=%s", tenant_id, date, action)
        return f"Calendar lookup failed for {date}. Please try again."

    # Token refresh write-back: if token changed after the API call, persist the update
    if creds.token and creds.token != token_before:
        try:
            new_token_dict = {
                "token": creds.token,
                "refresh_token": creds.refresh_token or token_dict.get("refresh_token", ""),
                "token_uri": token_dict.get("token_uri", _GOOGLE_TOKEN_URL),
                "client_id": token_dict.get("client_id", ""),
                "client_secret": token_dict.get("client_secret", ""),
                "scopes": token_dict.get("scopes", [_CALENDAR_SCOPE]),
            }
            new_encrypted = enc_svc.encrypt(json.dumps(new_token_dict))
            conn.config = {"token": new_encrypted}
            await session.commit()
            logger.debug("Calendar token refreshed and written back for tenant=%s", tenant_id)
        except Exception:
            logger.exception("Failed to write back refreshed calendar token for tenant=%s", tenant_id)
            # Non-fatal: the API call succeeded, just log the refresh failure

    return result_str


def _execute_calendar_action(
    creds: Any,
    action: str,
    date: str,
    calendar_id: str,
    event_summary: str | None,
    event_start: str | None,
    event_end: str | None,
) -> str:
    """
    Synchronous calendar action — runs in thread executor to avoid blocking.

    Args:
        creds:         Google Credentials object.
        action:        One of "list", "check_availability", "create".
        date:          Date in YYYY-MM-DD format.
        calendar_id:   Google Calendar ID (default "primary").
        event_summary: Title for create action.
        event_start:   ISO 8601 start for create action.
        event_end:     ISO 8601 end for create action.

    Returns:
        Natural language result string.
    """
    if build is None:
        return (
            "Google Calendar library not installed. "
            "Run: uv add google-api-python-client google-auth"
        )

    try:
        service = build("calendar", "v3", credentials=creds, cache_discovery=False)
    except Exception as exc:
        logger.warning("Failed to build Google Calendar service: %s", exc)
        return f"Calendar service error: {exc}"

    if action == "create":
        return _action_create(service, calendar_id, event_summary, event_start, event_end)
    elif action == "check_availability":
        return _action_check_availability(service, calendar_id, date)
    else:
        # Default: "list"
        return _action_list(service, calendar_id, date)


def _time_boundaries(date: str) -> tuple[str, str]:
    """Return (time_min, time_max) RFC3339 strings for the full given day (UTC)."""
    return f"{date}T00:00:00Z", f"{date}T23:59:59Z"


def _format_event_time(event: dict[str, Any]) -> str:
    """Extract and format the start time of a calendar event."""
    start = event.get("start", {})
    raw = start.get("dateTime") or start.get("date") or "Unknown time"
    # Trim the timezone part for readability if full datetime
    if "T" in raw:
        try:
            # e.g. "2026-03-26T09:00:00+00:00" → "09:00"
            time_part = raw.split("T")[1][:5]
            return time_part
        except IndexError:
            return raw
    return raw


def _action_list(service: Any, calendar_id: str, date: str) -> str:
    """List calendar events for the given date."""
    time_min, time_max = _time_boundaries(date)
    try:
        events_result = (
            service.events()
            .list(
                calendarId=calendar_id,
                timeMin=time_min,
                timeMax=time_max,
                singleEvents=True,
                orderBy="startTime",
            )
            .execute()
        )
    except Exception as exc:
        logger.warning("Google Calendar list error: %s", exc)
        return f"Calendar error listing events for {date}: {exc}"

    items = events_result.get("items", [])
    if not items:
        return f"No events found on {date}."

    lines = [f"Calendar events for {date}:"]
    for event in items:
        time_str = _format_event_time(event)
        summary = event.get("summary", "Untitled event")
        lines.append(f"- {time_str}: {summary}")

    return "\n".join(lines)


def _action_check_availability(service: Any, calendar_id: str, date: str) -> str:
    """Return a free/busy summary for the given date."""
    time_min, time_max = _time_boundaries(date)
    try:
        events_result = (
            service.events()
            .list(
                calendarId=calendar_id,
                timeMin=time_min,
                timeMax=time_max,
                singleEvents=True,
                orderBy="startTime",
            )
            .execute()
        )
    except Exception as exc:
        logger.warning("Google Calendar availability check error: %s", exc)
        return f"Calendar error checking availability for {date}: {exc}"

    items = events_result.get("items", [])
    if not items:
        return f"No events on {date} — the entire day is free."

    lines = [f"Busy slots on {date}:"]
    for event in items:
        time_str = _format_event_time(event)
        summary = event.get("summary", "Untitled event")
        lines.append(f"- {time_str}: {summary}")

    return "\n".join(lines)


def _action_create(
    service: Any,
    calendar_id: str,
    event_summary: str | None,
    event_start: str | None,
    event_end: str | None,
) -> str:
    """Create a new calendar event."""
    if not event_summary or not event_start or not event_end:
        missing = []
        if not event_summary:
            missing.append("event_summary")
        if not event_start:
            missing.append("event_start")
        if not event_end:
            missing.append("event_end")
        return f"Cannot create event: missing required fields: {', '.join(missing)}."

    event_body = {
        "summary": event_summary,
        "start": {"dateTime": event_start},
        "end": {"dateTime": event_end},
    }

    try:
        created = (
            service.events()
            .insert(calendarId=calendar_id, body=event_body)
            .execute()
        )
    except Exception as exc:
        logger.warning("Google Calendar create error: %s", exc)
        return f"Failed to create calendar event: {exc}"

    summary = created.get("summary", event_summary)
    start = created.get("start", {}).get("dateTime", event_start)
    end = created.get("end", {}).get("dateTime", event_end)
    return f"Event created: {summary} from {start} to {end}."
