from __future__ import annotations

import logging
import re
from importlib.metadata import PackageNotFoundError, version
from typing import Any

import httpx

logger = logging.getLogger(__name__)

DEFAULT_BASE_URL = "https://dropmail.me/api/graphql"


def _user_agent() -> str:
    try:
        v = version("dropmail-mcp")
    except PackageNotFoundError:
        v = "dev"
    return f"dropmail-mcp/{v}"


def _minify_query(query: str) -> str:
    q = re.sub(r"\s+", " ", query)
    q = re.sub(r"\s*([{}(),:]) *", r"\1", q)
    return q.strip()


class DropmailError(Exception):
    def __init__(self, message: str, code: str | None = None) -> None:
        super().__init__(message)
        self.code = code


class DropmailClient:
    def __init__(self, token: str, base_url: str = DEFAULT_BASE_URL) -> None:
        self._url = f"{base_url.rstrip('/')}/{token}"
        self._http = httpx.AsyncClient(
            headers={"User-Agent": _user_agent()},
        )

    async def execute(
        self,
        query: str,
        variables: dict[str, Any] | None = None,
        timeout: float = 30.0,
    ) -> tuple[dict[str, Any] | None, list[dict[str, Any]] | None]:
        """POST a GraphQL query, mutation, or subscription.

        For subscriptions (long-poll), pass a timeout longer than the server's
        poll window (e.g. 190.0 for a 180 s wait). The server blocks until a
        result is ready, then returns a plain JSON response and closes.
        Returns raw (data, errors) tuple without interpretation.
        """
        payload: dict[str, Any] = {"query": _minify_query(query)}
        if variables:
            payload["variables"] = variables
        resp = await self._http.post(self._url, json=payload, timeout=timeout)
        resp.raise_for_status()
        body = resp.json()
        logger.debug("GQL query=%s vars=%s response=%s", payload["query"], variables, body)
        return body.get("data"), body.get("errors")

    def unwrap(
        self,
        data: dict[str, Any] | None,
        errors: list[dict[str, Any]] | None,
        field: str,
    ) -> Any:
        """Return data[field], or raise DropmailError if it is null.

        Non-fatal errors on nested fields are ignored when the top-level field
        is present. Only raises when the requested field itself is null.
        """
        value = (data or {}).get(field)
        if value is not None:
            return value
        for e in errors or []:
            path = e.get("path") or []
            if not path or path[0] == field:
                code = (e.get("extensions") or {}).get("code")
                if code == "RATE_LIMIT_EXCEEDED":
                    raise DropmailError(
                        "Rate limit exceeded. Please try again later.", code
                    )
                if code == "SESSION_NOT_FOUND":
                    raise DropmailError(
                        "Session not found or expired. "
                        "Use create_session to start a new session. "
                        "If you have a saved restore_key, you can then call restore_address to recover previous addresses into the new session.",
                        code,
                    )
                raise DropmailError(e.get("message", "Unknown error"), code)
        raise DropmailError(f"'{field}' returned null")

    async def aclose(self) -> None:
        await self._http.aclose()
