"""GraphQL client with streaming pagination for memory-efficient data fetching."""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/08_utils_graphql.ipynb.

# %% ../nbs/08_utils_graphql.ipynb #c6e0f9f1
from __future__ import annotations
from .utils_api import AsyncAPIClient, bearer_token_auth
from typing import AsyncGenerator, Dict, Any, List, Optional
from contextlib import asynccontextmanager
import logging
from nbdev.showdoc import show_doc

logger = logging.getLogger(__name__)

# %% auto #0
__all__ = ['logger', 'GraphQLClient', 'execute_graphql']

# %% ../nbs/08_utils_graphql.ipynb #fedfdd43
class GraphQLClient:
    """GraphQL client with streaming pagination for memory-efficient data fetching."""
    
    def __init__(
        self,
        api_client: AsyncAPIClient, # Initialized AsyncAPIClient instance
        endpoint: str = '' # GraphQL endpoint path (default: '' for base URL)
    ):
        self.api_client = api_client
        self.endpoint = endpoint
        self._owns_api = False  # Track if we created the API client
    
    @classmethod
    @asynccontextmanager
    async def from_url(
        cls,
        url: str, # GraphQL endpoint URL
        bearer_token: str | None = None, # Optional bearer token for Authorization header
        headers: dict | None = None # Optional additional headers
    ):
        """Create GraphQL client directly from URL with optional bearer token.
        
        Usage:
            async with GraphQLClient.from_url(url, bearer_token=token) as gql:
                result = await gql.execute(query)
        """
        # Merge auth with any additional headers
        auth_headers = {}
        if bearer_token:
            auth_headers.update(bearer_token_auth(bearer_token))
        if headers:
            auth_headers.update(headers)
        
        async with AsyncAPIClient(
            url,
            auth_headers=auth_headers or None
        ) as api:
            client = cls(api)
            client._owns_api = True
            yield client
    
    async def execute(
        self,
        query: str, # GraphQL query or mutation string
        variables: dict = None # Query/mutation variables
    ) -> Dict[str, Any]:
        """Execute a GraphQL query or mutation and return the data portion.
        
        This is a unified method that works for both queries and mutations.
        Returns only the 'data' portion of the response for convenience.
        """
        payload = {'query': query}
        if variables:
            payload['variables'] = variables
        
        response = await self.api_client.request(
            method='POST',
            endpoint=self.endpoint,
            json=payload
        )
        
        data = response.json()
        
        # Check for GraphQL errors
        if 'errors' in data:
            error_msg = data['errors'][0].get('message', 'Unknown GraphQL error')
            raise ValueError(f"GraphQL error: {error_msg}")
        
        return data.get('data', {})
    
    async def execute_query(
        self,
        query: str, # GraphQL query string
        variables: dict = None # Query variables
    ) -> Dict[str, Any]:
        """Execute a single GraphQL query and return the JSON response."""
        payload = {'query': query}
        if variables:
            payload['variables'] = variables
        
        response = await self.api_client.request(
            method='POST',
            endpoint=self.endpoint,
            json=payload
        )
        
        data = response.json()
        
        # Check for GraphQL errors
        if 'errors' in data:
            error_msg = data['errors'][0].get('message', 'Unknown GraphQL error')
            raise ValueError(f"GraphQL error: {error_msg}")
        
        return data
    
    async def execute_mutation(
        self,
        mutation: str, # GraphQL mutation string
        variables: dict = None # Mutation variables
    ) -> Dict[str, Any]:
        """Execute a GraphQL mutation (alias for execute_query)."""
        return await self.execute_query(mutation, variables)
    
    async def fetch_pages_relay(
        self,
        query: str, # GraphQL query with $first and $after variables
        connection_path: str, # Dot-notation path to connection object (e.g., "transactionsConnection")
        variables: dict | None = None, # Base variables for the query (excluding first/after)
        page_size: int = 100, # Number of items per page
        max_pages: int | None = None # Maximum pages to fetch (None for unlimited)
    ) -> list[dict]:
        """Fetch all pages from a Relay-style paginated GraphQL query.
        
        Example query structure:
            query($first: Int, $after: String, $filter: TransactionFilter) {
                transactionsConnection(first: $first, after: $after, filter: $filter) {
                    edges { node { id amount } }
                    pageInfo { hasNextPage endCursor }
                }
            }
        
        Returns:
            List of all nodes from all pages
        """
        all_nodes = []
        cursor = None
        page_count = 0
        base_vars = variables or {}
        
        while True:
            if max_pages and page_count >= max_pages:
                logger.info(f"Reached max_pages limit of {max_pages}")
                break
            
            page_vars = {**base_vars, "first": page_size, "after": cursor}
            result = await self.execute(query, page_vars)
            
            # Navigate to connection using dot-notation path
            connection = result
            for key in connection_path.split('.'):
                connection = connection.get(key, {})
            
            edges = connection.get('edges', [])
            page_info = connection.get('pageInfo', {})
            
            for edge in edges:
                all_nodes.append(edge['node'])
            
            page_count += 1
            logger.info(f"Fetched page {page_count} with {len(edges)} edges")
            
            if not page_info.get('hasNextPage'):
                break
            
            cursor = page_info.get('endCursor')
        
        return all_nodes
    
    async def fetch_pages_generator(
        self,
        query_template: str, # GraphQL query with $variables placeholders
        variables: dict, # Initial variables (must include cursor key)
        items_path: list[str], # JSONPath to list of items (e.g., ['data', 'users', 'nodes'])
        cursor_path: list[str], # JSONPath to next cursor (e.g., ['data', 'users', 'pageInfo', 'endCursor'])
        has_next_path: list[str] = None, # Optional path to hasNextPage boolean (if None, checks cursor != None)
        cursor_var: str = 'cursor' # Variable name for cursor in query (default: 'cursor')
    ) -> AsyncGenerator[List[Dict], None]:
        """Stream paginated GraphQL data page-by-page using async generator."""
        has_next = True
        page_count = 0
        
        while has_next:
            # Execute query with current cursor
            response = await self.execute_query(query_template, variables)
            
            # Extract items using path
            items = self._get_nested_value(response, items_path)
            if not items:
                logger.warning(f"No items found at path {items_path}")
                break
            
            page_count += 1
            logger.info(f"Fetched page {page_count} with {len(items)} items")
            
            # Yield batch (CRITICAL: do not accumulate)
            yield items
            
            # Check if there's a next page
            if has_next_path:
                has_next = self._get_nested_value(response, has_next_path)
            else:
                # Fallback: check if cursor exists
                next_cursor = self._get_nested_value(response, cursor_path)
                has_next = next_cursor is not None
            
            # Update cursor for next iteration
            if has_next:
                next_cursor = self._get_nested_value(response, cursor_path)
                variables[cursor_var] = next_cursor
    
    def _get_nested_value(
        self, 
        data: dict, 
        path: list[str]
    ) -> Any:
        """Extract nested value from dict using path list."""
        result = data
        for key in path:
            if isinstance(result, dict) and key in result:
                result = result[key]
            else:
                return None
        return result

# %% ../nbs/08_utils_graphql.ipynb #dff81c4f
async def execute_graphql(
    url: str, # GraphQL endpoint URL
    query: str, # GraphQL query string
    variables: dict | None = None, # Optional query variables
    bearer_token: str | None = None, # Optional bearer token for Authorization header
    headers: dict | None = None # Optional additional headers
) -> dict:
    """Execute a single GraphQL query without managing client lifecycle.
    
    This is a convenience function for one-off queries. For multiple queries,
    use GraphQLClient.from_url() to reuse the connection.
    
    Usage:
        result = await execute_graphql(
            url="https://api.example.com/graphql",
            query="query { users { id name } }",
            bearer_token="your-token"
        )
    
    Raises:
        ValueError: If the response contains GraphQL errors
    """
    async with GraphQLClient.from_url(url, bearer_token=bearer_token, headers=headers) as gql:
        return await gql.execute(query, variables)
