from __future__ import annotations

import asyncio
from datetime import datetime, timedelta
from typing import Optional, Tuple

import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from .. import settings
from ..api.progress import get_redis
from ..models import GoogleSheetsIntegration, IntegrationStatus
from ..security.crypto import decrypt_str, encrypt_str

GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_REVOKE_URL = "https://oauth2.googleapis.com/revoke"
GOOGLE_SHEETS_SCOPE = "https://www.googleapis.com/auth/spreadsheets"


def get_google_sheets_client_id() -> str:
    return (settings.GOOGLE_SHEETS_CLIENT_ID or settings.GOOGLE_CLIENT_ID or "").strip()


def get_google_sheets_client_secret() -> str:
    return (settings.GOOGLE_SHEETS_CLIENT_SECRET or settings.GOOGLE_CLIENT_SECRET or "").strip()


def get_google_sheets_refresh_clients() -> list[tuple[str, str]]:
    candidates: list[tuple[str, str]] = []
    seen: set[tuple[str, str]] = set()
    primary = (get_google_sheets_client_id(), get_google_sheets_client_secret())
    shared = (
        (settings.GOOGLE_CLIENT_ID or "").strip(),
        (settings.GOOGLE_CLIENT_SECRET or "").strip(),
    )
    for candidate in (primary, shared):
        if not all(candidate) or candidate in seen:
            continue
        seen.add(candidate)
        candidates.append(candidate)
    return candidates


def _maybe_encrypt(value: Optional[str]) -> Optional[str]:
    if not value:
        return None
    if settings.ENCRYPTION_KEY:
        return encrypt_str(value)
    return value


def _maybe_decrypt(value: Optional[str]) -> Optional[str]:
    if not value:
        return None
    if settings.ENCRYPTION_KEY:
        return decrypt_str(value)
    return value


async def get_google_sheets_integration(
    account_id: str, db: AsyncSession
) -> Optional[GoogleSheetsIntegration]:
    result = await db.execute(
        select(GoogleSheetsIntegration).where(
            GoogleSheetsIntegration.account_id == str(account_id)
        )
    )
    return result.scalar_one_or_none()


async def upsert_google_sheets_integration(
    account_id: str,
    access_token: str,
    refresh_token: Optional[str],
    expires_in: Optional[int],
    scopes: Optional[list[str]] = None,
    user_email: Optional[str] = None,
    db: AsyncSession = None,
) -> GoogleSheetsIntegration:
    if db is None:
        raise ValueError("db session is required")

    integration = await get_google_sheets_integration(account_id, db)
    if not integration:
        integration = GoogleSheetsIntegration(account_id=str(account_id))
        db.add(integration)

    integration.status = IntegrationStatus.CONNECTED
    integration.access_token = _maybe_encrypt(access_token)
    if refresh_token:
        integration.refresh_token = _maybe_encrypt(refresh_token)
    integration.token_expires_at = (
        datetime.utcnow() + timedelta(seconds=expires_in or 3600)
        if expires_in
        else None
    )
    integration.scopes = scopes or integration.scopes
    integration.user_email = user_email or integration.user_email
    integration.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(integration)
    return integration


async def refresh_google_access_token(
    integration: GoogleSheetsIntegration, db: AsyncSession
) -> str:
    refresh_token = _maybe_decrypt(integration.refresh_token)
    if not refresh_token:
        raise RuntimeError("Google refresh token missing")

    redis_conn = await get_redis()
    lock_key = f"google_token_refresh:{integration.account_id}"
    acquired = False

    if redis_conn:
        acquired = await redis_conn.set(lock_key, "1", nx=True, ex=10)
        if not acquired:
            await asyncio.sleep(2)
            await db.refresh(integration)
            token = _maybe_decrypt(integration.access_token)
            if integration.token_expires_at and integration.token_expires_at > datetime.utcnow():
                if token:
                    return token
            raise RuntimeError("Concurrent token refresh in progress")

    try:
        last_error: RuntimeError | None = None
        data = None
        for client_id, client_secret in get_google_sheets_refresh_clients():
            async with httpx.AsyncClient(timeout=20.0) as client:
                resp = await client.post(
                    GOOGLE_TOKEN_URL,
                    data={
                        "client_id": client_id,
                        "client_secret": client_secret,
                        "refresh_token": refresh_token,
                        "grant_type": "refresh_token",
                    },
                )
            if resp.is_success:
                data = resp.json()
                break
            last_error = RuntimeError(
                f"Google token refresh failed ({resp.status_code}): {resp.text[:200]}"
            )
        if data is None:
            if last_error:
                raise last_error
            raise RuntimeError("Google token refresh failed: no OAuth client configured")

        access_token = data.get("access_token")
        expires_in = int(data.get("expires_in", 3600))
        if not access_token:
            raise RuntimeError("Google refresh response missing access_token")

        integration.access_token = _maybe_encrypt(access_token)
        integration.token_expires_at = datetime.utcnow() + timedelta(seconds=expires_in)
        integration.status = IntegrationStatus.CONNECTED
        integration.updated_at = datetime.utcnow()
        await db.commit()
        await db.refresh(integration)
        return access_token
    finally:
        if redis_conn and acquired:
            try:
                await redis_conn.delete(lock_key)
            except Exception:
                pass


async def revoke_google_token(token: str) -> None:
    if not token:
        return
    async with httpx.AsyncClient(timeout=10.0) as client:
        await client.post(GOOGLE_REVOKE_URL, data={"token": token})


async def ensure_google_access_token(
    account_id: str, db: AsyncSession
) -> Tuple[Optional[str], Optional[GoogleSheetsIntegration]]:
    integration = await get_google_sheets_integration(account_id, db)
    if not integration or integration.status != IntegrationStatus.CONNECTED:
        return None, integration

    access_token = _maybe_decrypt(integration.access_token)
    if not access_token:
        if integration.refresh_token:
            access_token = await refresh_google_access_token(integration, db)
        else:
            return None, integration

    if integration.token_expires_at:
        refresh_at = integration.token_expires_at - timedelta(seconds=60)
        if refresh_at <= datetime.utcnow():
            access_token = await refresh_google_access_token(integration, db)

    return access_token, integration
