"""A tenant's connected Microsoft Teams account: reading it, refreshing its
access token when expired, and everything below this module needs to call
Microsoft Graph.

Same shape as google_calendar.py, and the same connect flow -- written by a
different service into the tenant's own galaxiq_tenants schema, provider
value 'teams'. This module only ever reads what it wrote.
"""
import logging
from datetime import datetime, timedelta, timezone

import httpx
from psycopg2 import sql

from app.core.config import settings
from app.services.infra.database import get_db_connection

logger = logging.getLogger(__name__)

TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
GRAPH_BASE = "https://graph.microsoft.com/v1.0"

PROVIDER = "teams"


def get_teams_connection(tenant_id: str):
    """This tenant's active Microsoft Teams connection, refreshed if its
    access token has expired.

    Returns {"access_token", "user_email", "timezone"} -- everything
    check_availability/create_event/cancel_event need. None covers every
    reason it cannot be used: no connection, a disconnected one, or a
    refresh failure (revoked consent).
    """
    try:
        conn = get_db_connection()
    except Exception as ex:
        logger.error(f"Could not read the Teams connection for {tenant_id}: {ex}")
        return None

    try:
        with conn.cursor() as cur:
            cur.execute(
                sql.SQL('SELECT "accessToken", "refreshToken", "expiresAt" FROM '
                        '{}.integrations WHERE provider = %s AND status = %s')
                .format(sql.Identifier(tenant_id)),
                (PROVIDER, "ACTIVE"))
            row = cur.fetchone()
    except Exception as ex:
        logger.error(f"Could not read the Teams connection for {tenant_id}: {ex}")
        return None
    finally:
        conn.close()

    if not row:
        return None

    access_token, refresh_token, expires_at = row
    if expires_at is not None and expires_at <= datetime.now(timezone.utc):
        access_token = _refresh(tenant_id, refresh_token)
        if not access_token:
            return None

    headers = {"Authorization": f"Bearer {access_token}"}
    try:
        settings_response = httpx.get(
            f"{GRAPH_BASE}/me/mailboxSettings", headers=headers, timeout=20)
        settings_response.raise_for_status()
        mailbox_settings = settings_response.json()

        me_response = httpx.get(f"{GRAPH_BASE}/me", headers=headers, timeout=20)
        me_response.raise_for_status()
        me = me_response.json()
    except Exception as ex:
        logger.error(f"Could not read Microsoft Graph profile for {tenant_id}: {ex}")
        return None

    return {
        "access_token": access_token,
        "user_email": me.get("mail") or me.get("userPrincipalName") or "",
        "timezone": mailbox_settings.get("timeZone") or "UTC",
    }


def _refresh(tenant_id: str, refresh_token: str):
    """Exchanges a refresh token for a new access token, and writes it back
    onto the same row. Returns None on failure."""
    try:
        response = httpx.post(
            TOKEN_URL,
            data={
                "client_id": settings.MS_TEAMS_CLIENT_ID,
                "client_secret": settings.MS_TEAMS_CLIENT_SECRET,
                "refresh_token": refresh_token,
                "grant_type": "refresh_token",
                "scope": "https://graph.microsoft.com/.default",
            },
            timeout=20,
        )
        response.raise_for_status()
        payload = response.json()
    except Exception as ex:
        logger.error(f"Could not refresh the Teams token for {tenant_id}: {ex}")
        return None

    access_token = payload.get("access_token")
    if not access_token:
        return None

    expires_at = datetime.now(timezone.utc) + timedelta(
        seconds=payload.get("expires_in") or 3600)

    try:
        conn = get_db_connection()
    except Exception as ex:
        logger.error(f"Could not persist the refreshed Teams token for {tenant_id}: {ex}")
        return access_token

    try:
        with conn.cursor() as cur:
            cur.execute(
                sql.SQL('UPDATE {}.integrations SET "accessToken" = %s, '
                        '"expiresAt" = %s WHERE provider = %s')
                .format(sql.Identifier(tenant_id)),
                (access_token, expires_at, PROVIDER))
        conn.commit()
    except Exception as ex:
        conn.rollback()
        logger.error(f"Could not persist the refreshed Teams token for {tenant_id}: {ex}")
    finally:
        conn.close()

    return access_token


SLOT_MINUTES = 30


def check_availability(connection: dict, start, end, duration_minutes: int = SLOT_MINUTES) -> list:
    """Open duration_minutes-long slots inside [start, end]. Returns [] on
    any API failure, same safe-failure reasoning as the Google Calendar
    module."""
    try:
        start_utc = start.astimezone(timezone.utc)
        end_utc = end.astimezone(timezone.utc)
        response = httpx.post(
            f"{GRAPH_BASE}/me/calendar/getSchedule",
            headers={"Authorization": f"Bearer {connection['access_token']}"},
            json={
                "schedules": [connection["user_email"]],
                "startTime": {"dateTime": start_utc.strftime("%Y-%m-%dT%H:%M:%S"), "timeZone": "UTC"},
                "endTime": {"dateTime": end_utc.strftime("%Y-%m-%dT%H:%M:%S"), "timeZone": "UTC"},
            },
            timeout=20,
        )
        response.raise_for_status()
        items = response.json()["value"][0].get("scheduleItems") or []

        busy = []
        for item in items:
            if item.get("status") != "busy":
                continue
            busy.append((
                datetime.fromisoformat(item["start"]["dateTime"]).replace(tzinfo=timezone.utc),
                datetime.fromisoformat(item["end"]["dateTime"]).replace(tzinfo=timezone.utc),
            ))

        slots = []
        cursor = start
        step = timedelta(minutes=duration_minutes)
        while cursor + step <= end:
            slot_end = cursor + step
            if not any(cursor < b_end and slot_end > b_start for b_start, b_end in busy):
                slots.append({"start": cursor, "end": slot_end})
            cursor += step
        return slots
    except Exception as ex:
        logger.error(f"Could not check Teams availability: {ex}")
        return []


def create_event(connection: dict, start, end, visitor_email: str, visitor_name: str):
    """Books the slot and creates the Teams meeting in one call
    (isOnlineMeeting=true) -- Graph returns the join link on the created
    event. Returns None on failure."""
    try:
        start_utc = start.astimezone(timezone.utc)
        end_utc = end.astimezone(timezone.utc)
        response = httpx.post(
            f"{GRAPH_BASE}/me/events",
            headers={"Authorization": f"Bearer {connection['access_token']}"},
            json={
                "subject": f"Meeting with {visitor_name}",
                "start": {"dateTime": start_utc.strftime("%Y-%m-%dT%H:%M:%S"), "timeZone": "UTC"},
                "end": {"dateTime": end_utc.strftime("%Y-%m-%dT%H:%M:%S"), "timeZone": "UTC"},
                "attendees": [{
                    "emailAddress": {"address": visitor_email, "name": visitor_name},
                    "type": "required",
                }],
                "isOnlineMeeting": True,
                "onlineMeetingProvider": "teamsForBusiness",
            },
            timeout=20,
        )
        response.raise_for_status()
        event = response.json()
        event_id = event.get("id")
        if not event_id:
            logger.error("Could not create the Teams event: response missing 'id'")
            return None
        online_meeting = event.get("onlineMeeting") or {}
        return {"event_id": event_id, "meeting_link": online_meeting.get("joinUrl")}
    except Exception as ex:
        logger.error(f"Could not create the Teams event: {ex}")
        return None


def cancel_event(connection: dict, visitor_email: str, near_time=None) -> bool:
    """Finds the event by attendee email (narrowed to a window around
    near_time when given) and deletes it. Returns False when zero or more
    than one event matches -- does not guess.

    Attendee matching is done client-side in Python, not via Graph's
    $filter -- `$filter=attendees/any(...)` is not a supported OData
    expression for the events collection and would likely 400. near_time
    narrowing uses /me/calendarView (the only endpoint that honours
    startDateTime/endDateTime as a time window) rather than /me/events,
    which silently ignores those two params.
    """
    try:
        if near_time is not None:
            window_start = (near_time - timedelta(hours=2)).astimezone(timezone.utc)
            window_end = (near_time + timedelta(hours=2)).astimezone(timezone.utc)
            response = httpx.get(
                f"{GRAPH_BASE}/me/calendarView",
                headers={"Authorization": f"Bearer {connection['access_token']}"},
                params={
                    "startDateTime": window_start.strftime("%Y-%m-%dT%H:%M:%S"),
                    "endDateTime": window_end.strftime("%Y-%m-%dT%H:%M:%S"),
                },
                timeout=20,
            )
        else:
            response = httpx.get(
                f"{GRAPH_BASE}/me/events",
                headers={"Authorization": f"Bearer {connection['access_token']}"},
                timeout=20,
            )
        response.raise_for_status()
        candidates = response.json().get("value") or []

        # Kept inside this try/except -- a malformed provider response (e.g.
        # `attendees` is null, or an entry isn't a dict) must fail the same
        # "never raise, return False" way the HTTP call and JSON parsing
        # above already do, not crash the whole chat turn.
        visitor_email_lower = visitor_email.lower()
        items = []
        for event in candidates:
            if not isinstance(event, dict):
                continue
            attendees = event.get("attendees")
            if not isinstance(attendees, list):
                continue
            matched = False
            for attendee in attendees:
                if not isinstance(attendee, dict):
                    continue
                email_address = attendee.get("emailAddress")
                if not isinstance(email_address, dict):
                    continue
                if (email_address.get("address") or "").lower() == visitor_email_lower:
                    matched = True
                    break
            if matched:
                items.append(event)
    except Exception as ex:
        logger.error(f"Could not search Teams events for cancellation: {ex}")
        return False

    if len(items) != 1:
        return False

    try:
        response = httpx.delete(
            f"{GRAPH_BASE}/me/events/{items[0]['id']}",
            headers={"Authorization": f"Bearer {connection['access_token']}"},
            timeout=20,
        )
        response.raise_for_status()
        return True
    except Exception as ex:
        logger.error(f"Could not delete the Teams event: {ex}")
        return False
