"""
GraphQL utilities for Metamist, allows you to:
    - construct queries using the `gql` function (which validates graphql syntax)
    - validate queries with metamist schema (by fetching the schema)
"""

import os
from json.decoder import JSONDecodeError
from typing import Any, Dict  # noqa: UP035

import backoff
from gql import Client
from gql import gql as gql_constructor
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.aiohttp import log as aiohttp_logger
from gql.transport.exceptions import TransportServerError
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.requests import log as requests_logger

# this does not import itself, it imports the module
from graphql import DocumentNode  # type: ignore
from requests.exceptions import HTTPError

import metamist.configuration
from cpg_utils.cloud import get_google_identity_token


_sync_client: Client | None = None
_async_client: Client | None = None


def get_local_schema() -> str:
    """Get the local schema"""
    with open(os.path.join(os.path.dirname(__file__), 'schema.graphql')) as f:  # noqa: PTH118, PTH120, PTH123
        contents = f.read()

    return contents


def get_sm_url():
    """Get the URL for the Metamist GraphQL API"""
    return os.path.join(metamist.configuration.sm_url, 'graphql')  # noqa: PTH118


def configure_sync_client(
    url: str = None,
    schema: str = None,
    auth_token: str = None,
    force_recreate=False,
    use_local_schema: bool = False,
):
    """Get sync gql Client"""
    global _sync_client  # noqa: PLW0603

    if use_local_schema:
        # for local schema, we basically don't allow requests
        schema = get_local_schema()
        return Client(schema=schema)

    if _sync_client and not force_recreate:
        return _sync_client

    env = os.getenv('SM_ENVIRONMENT', 'PRODUCTION').lower()
    if env == 'local':
        transport = RequestsHTTPTransport(url=url or get_sm_url())
    else:
        token = auth_token or get_google_identity_token(
            target_audience=metamist.configuration.sm_url
        )
        transport = RequestsHTTPTransport(
            url=url or get_sm_url(),
            headers={'Authorization': f'Bearer {token}'},
        )

    _sync_client = Client(
        transport=transport, schema=schema, fetch_schema_from_transport=schema is None
    )
    # _sync_client.connect_sync()

    return _sync_client


async def configure_async_client(
    url: str = None, schema: str = None, auth_token: str = None, force_recreate=False
) -> Client:
    """Configure an async client for use with the Metamist GraphQL API"""
    global _async_client  # noqa: PLW0603

    if _async_client and not force_recreate:
        return _async_client

    env = os.getenv('SM_ENVIRONMENT', 'PRODUCTION').lower()
    if env == 'local':
        transport = AIOHTTPTransport(url=url or get_sm_url())
    else:
        token = auth_token or get_google_identity_token(
            target_audience=metamist.configuration.sm_url
        )
        transport = AIOHTTPTransport(
            url=url or get_sm_url(),
            headers={'Authorization': f'Bearer {token}'},
        )

    _async_client = Client(
        transport=transport, schema=schema, fetch_schema_from_transport=schema is None
    )
    # await _async_client.connect_async()
    return _async_client


async def get_async_client():
    """Get async gql Client"""
    return await configure_async_client()


def gql(
    request_string: str, should_validate=False, use_local_schema=False
) -> DocumentNode:
    """
    Given a String containing a GraphQL request, parse it into a Document.
    Optionally validate by fetching schema from metamist at SM_URL / SM_ENVIRONMENT
    """
    doc = gql_constructor(request_string)
    if should_validate:
        validate(doc, use_local_schema=use_local_schema)
    return doc


def validate(doc: DocumentNode, client=None, use_local_schema=False):
    """
    Validate a GraphQL document against the schema.
    Fetch schema from SM_URL / SM_ENVIRONMENT
    """
    if client is not None and use_local_schema:
        raise ValueError('Cannot provide local_schema and client to validate')
    if isinstance(doc, str):
        doc = gql(doc)

    (client or configure_sync_client(use_local_schema=use_local_schema)).validate(doc)
    return True


# use older style typing to broaden supported Python versions
@backoff.on_exception(
    backoff.expo,
    factor=3,
    jitter=None,
    exception=(HTTPError, JSONDecodeError, TransportServerError),
    max_tries=5,
    max_time=60,
)
def query(
    _query: str | DocumentNode,
    variables: dict | None = None,
    client: Client | None = None,
    log_response: bool = False,
) -> dict[str, Any]:
    """Query the metamist GraphQL API"""
    if variables is None:
        variables = {}

    # disable logging for gql
    current_level = aiohttp_logger.level
    if not log_response:
        requests_logger.setLevel('WARNING')

    response = (client or configure_sync_client()).execute_sync(
        _query if isinstance(_query, DocumentNode) else gql(_query),
        variable_values=variables,
    )

    if not log_response:
        requests_logger.setLevel(current_level)
    return response


@backoff.on_exception(
    backoff.expo,
    factor=3,
    jitter=None,
    exception=(HTTPError, JSONDecodeError, TransportServerError),
    max_tries=5,
    max_time=60,
)
async def query_async(
    _query: str | DocumentNode,
    variables: dict | None = None,
    client: Client | None = None,
    log_response: bool = False,
) -> dict[str, Any]:
    """Asynchronously query the Metamist GraphQL API"""
    if variables is None:
        variables = {}

    # disable logging for gql
    current_level = aiohttp_logger.level
    if log_response:
        aiohttp_logger.setLevel('WARNING')

    if not client:
        client = await configure_async_client()

    response = await client.execute_async(
        _query if isinstance(_query, DocumentNode) else gql(_query),
        variable_values=variables,
    )

    if log_response:
        aiohttp_logger.setLevel(current_level)

    return response
