"""GraphQL execution layer.

SOLID notes:
- The rest of the code depends only on the `GraphQLClient` interface
  (Dependency Inversion), not on `requests`.
- `RequestsGraphQLClient` is an Adapter that implements the interface.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Mapping, Protocol

import requests

from zenhub_client.constants import ZENHUB_GRAPHQL_ENDPOINT
from zenhub_client.exceptions import ZenhubGraphQLError, ZenhubTransportError


class GraphQLClient(Protocol):
    """Minimal interface for executing a GraphQL operation."""

    def execute(self, query: str, variables: Mapping[str, Any] | None = None) -> dict[str, Any]:
        ...


@dataclass(frozen=True, slots=True)
class GraphQLResponse:
    """Normalised response wrapper."""

    data: dict[str, Any]


class RequestsGraphQLClient:
    """Requests-based GraphQL client for Zenhub.

    This class intentionally has a narrow surface area: only `execute`.
    """

    def __init__(
        self,
        api_token: str,
        *,
        endpoint: str = ZENHUB_GRAPHQL_ENDPOINT,
        timeout_s: int = 30,
        session: requests.Session | None = None,
    ):
        self._endpoint = str(endpoint)
        self._timeout_s = int(timeout_s)
        self._session = session or requests.Session()
        self._session.headers.update(
            {
                "Authorization": f"Bearer {api_token}",
                "Content-Type": "application/json",
            }
        )

    def close(self) -> None:
        """Close the underlying HTTP session."""
        self._session.close()

    def execute(self, query: str, variables: Mapping[str, Any] | None = None) -> dict[str, Any]:
        payload = {
            "query": str(query),
            "variables": dict(variables) if variables is not None else dict(),
        }
        try:
            response = self._session.post(
                self._endpoint,
                json=payload,
                timeout=self._timeout_s,
            )
            response.raise_for_status()
        except requests.RequestException as exc:
            raise ZenhubTransportError(str(exc)) from exc

        try:
            body = response.json()
        except ValueError as exc:
            raise ZenhubTransportError("Zenhub returned non-JSON response") from exc

        if isinstance(body, dict) and body.get("errors"):
            raise ZenhubGraphQLError("GraphQL errors returned by Zenhub", errors=body.get("errors"))

        if not isinstance(body, dict) or "data" not in body:
            raise ZenhubTransportError(f"Unexpected response shape: {body!r}")

        data = body.get("data")
        if not isinstance(data, dict):
            raise ZenhubTransportError(f"Unexpected data shape: {data!r}")

        return data
