import base64
import json
import os
import smtplib
from dataclasses import dataclass
from email.message import Message
from urllib.parse import urlencode

import httpx
from cryptography.fernet import Fernet, InvalidToken
from sqlalchemy import select

from app.core.models import SystemConfig


@dataclass(frozen=True)
class OAuthProvider:
    authorization_endpoint: str
    token_endpoint: str
    scopes: tuple[str, ...]


PROVIDERS = {
    "google": OAuthProvider(
        authorization_endpoint="https://accounts.google.com/o/oauth2/v2/auth",
        token_endpoint="https://oauth2.googleapis.com/token",
        scopes=("https://www.googleapis.com/auth/gmail.send", "openid", "email", "profile"),
    ),
    "microsoft": OAuthProvider(
        authorization_endpoint="https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
        token_endpoint="https://login.microsoftonline.com/common/oauth2/v2.0/token",
        scopes=("https://outlook.office.com/SMTP.Send", "offline_access", "openid", "email", "profile"),
    ),
}


def _env_name(provider: str, suffix: str) -> str:
    return f"{provider.upper()}_OAUTH_{suffix}"


def provider_configured(provider: str) -> bool:
    return provider in PROVIDERS and bool(
        os.getenv(_env_name(provider, "CLIENT_ID"))
        and os.getenv(_env_name(provider, "CLIENT_SECRET"))
        and os.getenv("PUBLIC_APP_URL")
    )


def callback_url(provider: str) -> str:
    if provider not in PROVIDERS or not os.getenv("PUBLIC_APP_URL"):
        raise ValueError("OAuth provider is not configured")
    return f"{os.environ['PUBLIC_APP_URL'].rstrip('/')}/api/config/email-oauth/{provider}/callback"


def build_authorization_url(provider: str, state: str) -> str:
    if not provider_configured(provider):
        raise ValueError("OAuth provider is not configured")
    config = PROVIDERS[provider]
    params = {
        "client_id": os.environ[_env_name(provider, "CLIENT_ID")],
        "redirect_uri": callback_url(provider),
        "response_type": "code",
        "scope": " ".join(config.scopes),
        "state": state,
    }
    if provider == "google":
        params.update(access_type="offline", prompt="consent")
    return f"{config.authorization_endpoint}?{urlencode(params)}"


def _fernet() -> Fernet:
    key = os.getenv("EMAIL_OAUTH_ENCRYPTION_KEY")
    if not key:
        raise RuntimeError("EMAIL_OAUTH_ENCRYPTION_KEY is not configured")
    return Fernet(key.encode())


def encrypt_refresh_token(value: str) -> str:
    return _fernet().encrypt(value.encode()).decode()


def decrypt_refresh_token(value: str) -> str:
    try:
        return _fernet().decrypt(value.encode()).decode()
    except InvalidToken as exc:
        raise RuntimeError("Stored email authorization is invalid. Reconnect the mailbox.") from exc


def get_oauth_credentials(db, tenant_id: int, provider: str) -> tuple[str, str] | None:
    rows = db.execute(
        select(SystemConfig).where(
            SystemConfig.tenant_id == tenant_id,
            SystemConfig.key_name.in_(("email_oauth_email", "email_oauth_provider", "email_oauth_refresh_token")),
        )
    ).scalars()
    values = {row.key_name: row for row in rows}
    email = values.get("email_oauth_email")
    stored_provider = values.get("email_oauth_provider")
    token = values.get("email_oauth_refresh_token")
    if not email or not email.key_value or not stored_provider or stored_provider.key_value != provider or not token or not token.encrypted_value:
        return None
    return email.key_value, decrypt_refresh_token(token.encrypted_value)


async def exchange_code(provider: str, code: str) -> dict:
    if not provider_configured(provider):
        raise ValueError("OAuth provider is not configured")
    async with httpx.AsyncClient(timeout=20) as client:
        response = await client.post(
            PROVIDERS[provider].token_endpoint,
            data={
                "client_id": os.environ[_env_name(provider, "CLIENT_ID")],
                "client_secret": os.environ[_env_name(provider, "CLIENT_SECRET")],
                "code": code,
                "grant_type": "authorization_code",
                "redirect_uri": callback_url(provider),
            },
        )
    response.raise_for_status()
    return response.json()


async def refresh_access_token(provider: str, refresh_token: str) -> str:
    if not provider_configured(provider):
        raise RuntimeError("OAuth provider is not configured")
    async with httpx.AsyncClient(timeout=20) as client:
        response = await client.post(
            PROVIDERS[provider].token_endpoint,
            data={
                "client_id": os.environ[_env_name(provider, "CLIENT_ID")],
                "client_secret": os.environ[_env_name(provider, "CLIENT_SECRET")],
                "refresh_token": refresh_token,
                "grant_type": "refresh_token",
            },
        )
    response.raise_for_status()
    return response.json()["access_token"]


def email_from_id_token(id_token: str) -> str:
    try:
        payload = id_token.split(".")[1]
        payload += "=" * (-len(payload) % 4)
        claims = json.loads(base64.urlsafe_b64decode(payload))
    except (IndexError, UnicodeDecodeError, ValueError) as exc:
        raise ValueError("OAuth provider did not return an email address") from exc
    return claims.get("email") or claims.get("preferred_username") or ""


def send_oauth_smtp(provider: str, sender_email: str, access_token: str, message: Message, to_email: str) -> None:
    if provider != "microsoft":
        raise ValueError("Unsupported email provider")
    xoauth2 = base64.b64encode(
        f"user={sender_email}\x01auth=Bearer {access_token}\x01\x01".encode()
    ).decode()
    server = smtplib.SMTP("smtp.office365.com", 587)
    try:
        server.starttls()
        server.ehlo()
        server.docmd("AUTH", f"XOAUTH2 {xoauth2}")
        server.sendmail(sender_email, [to_email], message.as_string())
    finally:
        server.quit()


def send_google_api(sender_email: str, access_token: str, message: Message) -> None:
    raw = base64.urlsafe_b64encode(message.as_bytes()).rstrip(b"=").decode()
    response = httpx.post(
        "https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
        headers={"Authorization": f"Bearer {access_token}"},
        json={"raw": raw},
        timeout=20,
    )
    response.raise_for_status()
