"""OAuth flow helpers for integrations providers."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import os
import secrets
import threading
import webbrowser
from urllib.parse import urlencode

import requests
from requests.exceptions import RequestException
from sqlalchemy.orm import Session

from app.services.integrations import service as integrations_service
from app.services.integrations.errors import IntegrationError

REQUEST_TIMEOUT_SECONDS = 20
OAUTH_STATE_TTL_SECONDS = 15 * 60

GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_TASK_LISTS_URL = "https://tasks.googleapis.com/tasks/v1/users/@me/lists"

MICROSOFT_AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
MICROSOFT_LISTS_URL = "https://graph.microsoft.com/v1.0/me/todo/lists"


@dataclass(slots=True)
class OAuthStartResult:
    """Data returned when starting OAuth flow."""

    provider_id: str
    auth_url: str
    state: str
    redirect_uri: str
    browser_opened: bool

    def as_dict(self) -> dict[str, object]:
        return {
            "provider": self.provider_id,
            "auth_url": self.auth_url,
            "state": self.state,
            "redirect_uri": self.redirect_uri,
            "browser_opened": self.browser_opened,
        }


@dataclass(slots=True)
class OAuthCallbackResult:
    """Data returned when OAuth callback completes."""

    provider_id: str
    detail: str
    settings_applied: dict[str, str]


@dataclass(slots=True)
class _OAuthPendingState:
    provider_id: str
    redirect_uri: str
    created_at: datetime


_STATE_LOCK = threading.Lock()
_PENDING_STATES: dict[str, _OAuthPendingState] = {}


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def _normalize_base_url(base_url: str) -> str:
    value = str(base_url or "").strip().rstrip("/")
    if not value.startswith("http"):
        raise IntegrationError("A valid base URL is required for OAuth flows")
    return value


def _cleanup_states() -> None:
    cutoff = _utc_now() - timedelta(seconds=OAUTH_STATE_TTL_SECONDS)
    stale = [key for key, item in _PENDING_STATES.items() if item.created_at < cutoff]
    for key in stale:
        _PENDING_STATES.pop(key, None)


def _require_provider(provider_id: str) -> str:
    provider = str(provider_id or "").strip().lower()
    if provider not in {"google_tasks", "microsoft_todo"}:
        raise IntegrationError(f"OAuth is not supported for provider '{provider_id}'")
    return provider


def _env_pair(provider_id: str) -> tuple[str, str]:
    if provider_id == "google_tasks":
        return "GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_SECRET"
    return "MICROSOFT_OAUTH_CLIENT_ID", "MICROSOFT_OAUTH_CLIENT_SECRET"


def _client_credentials(provider_id: str) -> tuple[str, str]:
    client_id_key, client_secret_key = _env_pair(provider_id)
    client_id = (os.getenv(client_id_key) or "").strip()
    client_secret = (os.getenv(client_secret_key) or "").strip()
    if not client_id or not client_secret:
        raise IntegrationError(
            f"Missing OAuth credentials. Set {client_id_key} and {client_secret_key}.",
        )
    return client_id, client_secret


def _google_auth_url(*, client_id: str, redirect_uri: str, state: str) -> str:
    query = urlencode(
        {
            "client_id": client_id,
            "redirect_uri": redirect_uri,
            "response_type": "code",
            "scope": "https://www.googleapis.com/auth/tasks",
            "access_type": "offline",
            "prompt": "consent",
            "include_granted_scopes": "true",
            "state": state,
        },
    )
    return f"{GOOGLE_AUTH_URL}?{query}"


def _microsoft_auth_url(*, client_id: str, redirect_uri: str, state: str) -> str:
    query = urlencode(
        {
            "client_id": client_id,
            "response_type": "code",
            "redirect_uri": redirect_uri,
            "response_mode": "query",
            "scope": "offline_access Tasks.ReadWrite User.Read",
            "state": state,
        },
    )
    return f"{MICROSOFT_AUTH_URL}?{query}"


def start_oauth(
    provider_id: str,
    *,
    base_url: str,
    open_browser: bool = True,
) -> OAuthStartResult:
    """Create OAuth authorization URL and optionally open system browser."""
    provider = _require_provider(provider_id)
    normalized_base = _normalize_base_url(base_url)
    client_id, _ = _client_credentials(provider)

    state = secrets.token_urlsafe(24)
    redirect_uri = f"{normalized_base}/integrations/oauth/callback/{provider}"
    if provider == "google_tasks":
        auth_url = _google_auth_url(client_id=client_id, redirect_uri=redirect_uri, state=state)
    else:
        auth_url = _microsoft_auth_url(client_id=client_id, redirect_uri=redirect_uri, state=state)

    with _STATE_LOCK:
        _cleanup_states()
        _PENDING_STATES[state] = _OAuthPendingState(
            provider_id=provider,
            redirect_uri=redirect_uri,
            created_at=_utc_now(),
        )

    browser_opened = False
    if open_browser:
        try:
            browser_opened = bool(webbrowser.open(auth_url, new=1))
        except Exception:
            browser_opened = False

    return OAuthStartResult(
        provider_id=provider,
        auth_url=auth_url,
        state=state,
        redirect_uri=redirect_uri,
        browser_opened=browser_opened,
    )


def _consume_state(provider_id: str, state: str) -> _OAuthPendingState:
    with _STATE_LOCK:
        _cleanup_states()
        pending = _PENDING_STATES.pop(state, None)
    if pending is None:
        raise IntegrationError("OAuth session expired or invalid state")
    if pending.provider_id != provider_id:
        raise IntegrationError("OAuth state/provider mismatch")
    return pending


def _google_exchange_code(*, code: str, redirect_uri: str, client_id: str, client_secret: str) -> dict[str, str]:
    data = {
        "code": code,
        "client_id": client_id,
        "client_secret": client_secret,
        "redirect_uri": redirect_uri,
        "grant_type": "authorization_code",
    }
    try:
        response = requests.post(
            GOOGLE_TOKEN_URL,
            data=data,
            timeout=REQUEST_TIMEOUT_SECONDS,
        )
    except RequestException as exc:
        raise IntegrationError(f"Google OAuth token exchange failed: {exc}") from exc
    if response.status_code >= 400:
        detail = response.text.strip() or "token exchange failed"
        raise IntegrationError(f"Google OAuth error ({response.status_code}): {detail}")
    body = response.json()
    access_token = str(body.get("access_token", "")).strip()
    if not access_token:
        raise IntegrationError("Google OAuth response did not include access_token")
    credentials = {"access_token": access_token}
    refresh_token = str(body.get("refresh_token", "")).strip()
    if refresh_token:
        credentials["refresh_token"] = refresh_token
    token_type = str(body.get("token_type", "")).strip()
    if token_type:
        credentials["token_type"] = token_type
    expires_in = body.get("expires_in")
    if isinstance(expires_in, int):
        credentials["expires_in"] = str(expires_in)
    return credentials


def _microsoft_exchange_code(*, code: str, redirect_uri: str, client_id: str, client_secret: str) -> dict[str, str]:
    data = {
        "client_id": client_id,
        "client_secret": client_secret,
        "code": code,
        "redirect_uri": redirect_uri,
        "grant_type": "authorization_code",
        "scope": "offline_access Tasks.ReadWrite User.Read",
    }
    try:
        response = requests.post(
            MICROSOFT_TOKEN_URL,
            data=data,
            timeout=REQUEST_TIMEOUT_SECONDS,
        )
    except RequestException as exc:
        raise IntegrationError(f"Microsoft OAuth token exchange failed: {exc}") from exc
    if response.status_code >= 400:
        detail = response.text.strip() or "token exchange failed"
        raise IntegrationError(f"Microsoft OAuth error ({response.status_code}): {detail}")
    body = response.json()
    access_token = str(body.get("access_token", "")).strip()
    if not access_token:
        raise IntegrationError("Microsoft OAuth response did not include access_token")
    credentials = {"access_token": access_token}
    refresh_token = str(body.get("refresh_token", "")).strip()
    if refresh_token:
        credentials["refresh_token"] = refresh_token
    token_type = str(body.get("token_type", "")).strip()
    if token_type:
        credentials["token_type"] = token_type
    expires_in = body.get("expires_in")
    if isinstance(expires_in, int):
        credentials["expires_in"] = str(expires_in)
    return credentials


def _discover_google_settings(access_token: str) -> dict[str, str]:
    headers = {"Authorization": f"Bearer {access_token}"}
    try:
        response = requests.get(
            GOOGLE_TASK_LISTS_URL,
            headers=headers,
            timeout=REQUEST_TIMEOUT_SECONDS,
        )
    except RequestException:
        return {}
    if response.status_code >= 400:
        return {}
    try:
        body = response.json()
    except ValueError:
        return {}
    items = body.get("items")
    if isinstance(items, list):
        for item in items:
            if not isinstance(item, dict):
                continue
            list_id = str(item.get("id", "")).strip()
            if list_id:
                return {"task_list_id": list_id}
    return {}


def _discover_microsoft_settings(access_token: str) -> dict[str, str]:
    headers = {"Authorization": f"Bearer {access_token}"}
    try:
        response = requests.get(
            MICROSOFT_LISTS_URL,
            headers=headers,
            timeout=REQUEST_TIMEOUT_SECONDS,
        )
    except RequestException:
        return {}
    if response.status_code >= 400:
        return {}
    try:
        body = response.json()
    except ValueError:
        return {}
    items = body.get("value")
    if isinstance(items, list):
        for item in items:
            if not isinstance(item, dict):
                continue
            list_id = str(item.get("id", "")).strip()
            if list_id:
                return {"list_id": list_id}
    return {}


def complete_oauth_callback(
    provider_id: str,
    *,
    state: str,
    code: str,
    db: Session | None = None,
) -> OAuthCallbackResult:
    """Validate OAuth callback and persist provider credentials."""
    provider = _require_provider(provider_id)
    state_value = str(state or "").strip()
    code_value = str(code or "").strip()
    if not state_value:
        raise IntegrationError("Missing OAuth state")
    if not code_value:
        raise IntegrationError("Missing OAuth code")

    pending = _consume_state(provider, state_value)
    client_id, client_secret = _client_credentials(provider)

    if provider == "google_tasks":
        credentials = _google_exchange_code(
            code=code_value,
            redirect_uri=pending.redirect_uri,
            client_id=client_id,
            client_secret=client_secret,
        )
        settings = _discover_google_settings(credentials["access_token"])
    else:
        credentials = _microsoft_exchange_code(
            code=code_value,
            redirect_uri=pending.redirect_uri,
            client_id=client_id,
            client_secret=client_secret,
        )
        settings = _discover_microsoft_settings(credentials["access_token"])

    integrations_service.save_provider_config(
        provider_id=provider,
        enabled=True,
        credentials=credentials,
        settings=settings,
        auth_mode="oauth",
        db=db,
    )
    detail = "OAuth connection completed successfully"
    return OAuthCallbackResult(provider_id=provider, detail=detail, settings_applied=settings)
