# app/services/letzshop/client_service.py
"""
GraphQL client for Letzshop marketplace API.

Handles authentication, request formatting, and error handling
for all Letzshop API operations.
"""

import logging
import time
from collections.abc import Callable
from typing import Any

import requests

logger = logging.getLogger(__name__)

# Default API endpoint
DEFAULT_ENDPOINT = "https://letzshop.lu/graphql"


class LetzshopClientError(Exception):
    """Base exception for Letzshop client errors."""

    def __init__(self, message: str, response_data: dict | None = None):
        super().__init__(message)
        self.message = message
        self.response_data = response_data


class LetzshopAuthError(LetzshopClientError):
    """Raised when authentication fails."""


class LetzshopAPIError(LetzshopClientError):
    """Raised when the API returns an error response."""


class LetzshopConnectionError(LetzshopClientError):
    """Raised when connection to the API fails."""


# ============================================================================
# GraphQL Queries
# ============================================================================

QUERY_SHIPMENTS_UNCONFIRMED = """
query {
    shipments(state: unconfirmed) {
        nodes {
            id
            number
            state
            order {
                id
                number
                email
                total
                completedAt
                locale
                shipAddress {
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {
                        name { en fr de }
                        iso
                    }
                }
                billAddress {
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {
                        name { en fr de }
                        iso
                    }
                }
            }
            inventoryUnits {
                id
                state
                variant {
                    id
                    sku
                    mpn
                    price
                    tradeId {
                        number
                        parser
                    }
                    product {
                        name {
                            en
                            fr
                            de
                        }
                        _brand {
                            ... on Brand {
                                name
                            }
                        }
                    }
                }
            }
            tracking {
                code
                provider
            }
            data {
                __typename
            }
        }
    }
}
"""

QUERY_SHIPMENTS_CONFIRMED = """
query {
    shipments(state: confirmed) {
        nodes {
            id
            number
            state
            order {
                id
                number
                email
                total
                completedAt
                locale
                shipAddress {
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {
                        name { en fr de }
                        iso
                    }
                }
                billAddress {
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {
                        name { en fr de }
                        iso
                    }
                }
            }
            inventoryUnits {
                id
                state
                variant {
                    id
                    sku
                    mpn
                    price
                    tradeId {
                        number
                        parser
                    }
                    product {
                        name {
                            en
                            fr
                            de
                        }
                        _brand {
                            ... on Brand {
                                name
                            }
                        }
                    }
                }
            }
            tracking {
                code
                provider
            }
            data {
                __typename
            }
        }
    }
}
"""

QUERY_SHIPMENT_BY_ID = """
query GetShipment($id: ID!) {
    node(id: $id) {
        ... on Shipment {
            id
            number
            state
            order {
                id
                number
                email
                total
                completedAt
                locale
                shipAddress {
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {
                        name { en fr de }
                        iso
                    }
                }
                billAddress {
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {
                        name { en fr de }
                        iso
                    }
                }
            }
            inventoryUnits {
                id
                state
                variant {
                    id
                    sku
                    mpn
                    price
                    tradeId {
                        number
                        parser
                    }
                    product {
                        name {
                            en
                            fr
                            de
                        }
                        _brand {
                            ... on Brand {
                                name
                            }
                        }
                    }
                }
            }
            tracking {
                code
                provider
            }
            data {
                __typename
            }
        }
    }
}
"""

# ============================================================================
# Paginated Queries (for historical import)
# ============================================================================

# Note: Using string formatting for state since Letzshop has issues with enum variables
# Note: tracking field removed - causes 'demodulize' server error on some shipments
QUERY_SHIPMENTS_PAGINATED_TEMPLATE = """
query GetShipmentsPaginated($first: Int!, $after: String) {{
    shipments(state: {state}, first: $first, after: $after) {{
        pageInfo {{
            hasNextPage
            endCursor
        }}
        nodes {{
            id
            number
            state
            order {{
                id
                number
                email
                total
                completedAt
                locale
                shipAddress {{
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {{
                        iso
                    }}
                }}
                billAddress {{
                    firstName
                    lastName
                    merchant
                    streetName
                    streetNumber
                    city
                    zipCode
                    phone
                    country {{
                        iso
                    }}
                }}
            }}
            inventoryUnits {{
                id
                state
                variant {{
                    id
                    sku
                    mpn
                    price
                    tradeId {{
                        number
                        parser
                    }}
                    product {{
                        name {{
                            en
                            fr
                            de
                        }}
                    }}
                }}
            }}
            data {{
                __typename
            }}
        }}
    }}
}}
"""

# ============================================================================
# GraphQL Queries - Store Directory (Public)
# ============================================================================

QUERY_STORES_PAGINATED = """
query GetStoresPaginated($first: Int!, $after: String) {
    stores(first: $first, after: $after) {
        pageInfo {
            hasNextPage
            endCursor
        }
        totalCount
        nodes {
            id
            slug
            name
            active
            merchantName
            legalName
            email
            phone
            fax
            homepage
            description { en fr de }
            location {
                street
                number
                city
                zipcode
                country { iso }
            }
            lat
            lng
            storeCategories { name { en fr de } }
            backgroundImage { url }
            socialMediaLinks { url }
            openingHours { en fr de }
            representative
            representativeTitle
        }
    }
}
"""

QUERY_STORE_BY_SLUG = """
query GetStoreBySlug($slug: String!) {
    store(slug: $slug) {
        id
        slug
        name
        active
        merchantName
        legalName
        email
        phone
        fax
        homepage
        description { en fr de }
        location {
            street
            number
            city
            zipcode
            country { iso }
        }
        lat
        lng
        storeCategories { name { en fr de } }
        backgroundImage { url }
        socialMediaLinks { url }
        openingHours { en fr de }
        representative
        representativeTitle
    }
}
"""

# ============================================================================
# GraphQL Mutations
# ============================================================================

MUTATION_CONFIRM_INVENTORY_UNITS = """
mutation ConfirmInventoryUnits($input: ConfirmInventoryUnitsInput!) {
    confirmInventoryUnits(input: $input) {
        inventoryUnits {
            id
            state
        }
        errors {
            id
            code
            message
        }
    }
}
"""

MUTATION_REJECT_INVENTORY_UNITS = """
mutation RejectInventoryUnits($input: RejectInventoryUnitsInput!) {
    returnInventoryUnits(input: $input) {
        inventoryUnits {
            id
            state
        }
        errors {
            id
            code
            message
        }
    }
}
"""

MUTATION_SET_SHIPMENT_TRACKING = """
mutation SetShipmentTracking($input: SetShipmentTrackingInput!) {
    setShipmentTracking(input: $input) {
        shipment {
            id
            tracking {
                code
                provider
            }
        }
        errors {
            code
            message
        }
    }
}
"""


class LetzshopClient:
    """
    GraphQL client for Letzshop marketplace API.

    Usage:
        client = LetzshopClient(api_key="your-api-key")
        shipments = client.get_shipments(state="unconfirmed")
    """

    def __init__(
        self,
        api_key: str,
        endpoint: str = DEFAULT_ENDPOINT,
        timeout: int = 30,
    ):
        """
        Initialize the Letzshop client.

        Args:
            api_key: The Letzshop API key (Bearer token).
            endpoint: The GraphQL endpoint URL.
            timeout: Request timeout in seconds.
        """
        self.api_key = api_key
        self.endpoint = endpoint
        self.timeout = timeout
        self._session: requests.Session | None = None

    @property
    def session(self) -> requests.Session:
        """Get or create a requests session."""
        if self._session is None:
            self._session = requests.Session()
            self._session.headers.update(
                {
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json",
                }
            )
        return self._session

    def close(self) -> None:
        """Close the HTTP session."""
        if self._session is not None:
            self._session.close()
            self._session = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
        return False

    def _execute_public(
        self,
        query: str,
        variables: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """
        Execute a GraphQL query without authentication (for public queries).

        Args:
            query: The GraphQL query string.
            variables: Optional variables for the query.

        Returns:
            The response data from the API.

        Raises:
            LetzshopAPIError: If the API returns an error.
            LetzshopConnectionError: If the request fails.
        """
        payload = {"query": query}
        if variables:
            payload["variables"] = variables

        logger.debug(f"Executing public GraphQL request to {self.endpoint}")

        try:
            # Use a simple request without Authorization header
            response = requests.post(
                self.endpoint,
                json=payload,
                headers={"Content-Type": "application/json"},
                timeout=self.timeout,
            )
        except requests.exceptions.Timeout as e:
            raise LetzshopConnectionError(f"Request timed out: {e}") from e
        except requests.exceptions.ConnectionError as e:
            raise LetzshopConnectionError(f"Connection failed: {e}") from e
        except requests.exceptions.RequestException as e:
            raise LetzshopConnectionError(f"Request failed: {e}") from e

        # Handle HTTP-level errors
        if response.status_code >= 500:
            raise LetzshopAPIError(
                f"Letzshop server error (HTTP {response.status_code})",
                response_data={"status_code": response.status_code},
            )

        # Parse JSON response
        try:
            data = response.json()
        except ValueError as e:
            raise LetzshopAPIError(
                f"Invalid JSON response: {response.text[:200]}"
            ) from e

        logger.debug(f"GraphQL response: {data}")

        # Handle GraphQL errors
        if "errors" in data:
            errors = data["errors"]
            error_messages = [e.get("message", str(e)) for e in errors]
            raise LetzshopAPIError(
                f"GraphQL errors: {'; '.join(error_messages)}",
                response_data=data,
            )

        return data.get("data", {})

    def _execute(
        self,
        query: str,
        variables: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """
        Execute a GraphQL query or mutation.

        Args:
            query: The GraphQL query or mutation string.
            variables: Optional variables for the query.

        Returns:
            The response data from the API.

        Raises:
            LetzshopAuthError: If authentication fails.
            LetzshopAPIError: If the API returns an error.
            LetzshopConnectionError: If the request fails.
        """
        payload = {"query": query}
        if variables:
            payload["variables"] = variables

        logger.debug(f"Executing GraphQL request to {self.endpoint}")

        try:
            response = self.session.post(
                self.endpoint,
                json=payload,
                timeout=self.timeout,
            )
        except requests.exceptions.Timeout as e:
            raise LetzshopConnectionError(f"Request timed out: {e}") from e
        except requests.exceptions.ConnectionError as e:
            raise LetzshopConnectionError(f"Connection failed: {e}") from e
        except requests.exceptions.RequestException as e:
            raise LetzshopConnectionError(f"Request failed: {e}") from e

        # Handle HTTP-level errors
        if response.status_code == 401:
            raise LetzshopAuthError(
                "Authentication failed. Please check your API key.",
                response_data={"status_code": 401},
            )
        if response.status_code == 403:
            raise LetzshopAuthError(
                "Access forbidden. Your API key may not have the required permissions.",
                response_data={"status_code": 403},
            )
        if response.status_code >= 500:
            raise LetzshopAPIError(
                f"Letzshop server error (HTTP {response.status_code})",
                response_data={"status_code": response.status_code},
            )

        # Parse JSON response
        try:
            data = response.json()
        except ValueError as e:
            raise LetzshopAPIError(
                f"Invalid JSON response: {response.text[:200]}"
            ) from e

        logger.debug(f"GraphQL response: {data}")

        # Check for GraphQL errors
        if "errors" in data and data["errors"]:
            error_messages = [
                err.get("message", "Unknown error") for err in data["errors"]
            ]
            logger.warning(f"GraphQL errors received: {data['errors']}")
            raise LetzshopAPIError(
                f"GraphQL errors: {'; '.join(error_messages)}",
                response_data=data,
            )

        return data.get("data", {})

    # ========================================================================
    # Connection Testing
    # ========================================================================

    def test_connection(self) -> tuple[bool, float, str | None]:
        """
        Test the connection to Letzshop API.

        Returns:
            Tuple of (success, response_time_ms, error_message).
        """
        test_query = """
        query TestConnection {
            __typename
        }
        """

        start_time = time.time()

        try:
            self._execute(test_query)
            elapsed_ms = (time.time() - start_time) * 1000
            return True, elapsed_ms, None
        except LetzshopClientError as e:
            elapsed_ms = (time.time() - start_time) * 1000
            return False, elapsed_ms, str(e)

    # ========================================================================
    # Shipment Queries
    # ========================================================================

    def get_shipments(
        self,
        state: str = "unconfirmed",
    ) -> list[dict[str, Any]]:
        """
        Get shipments from Letzshop.

        Args:
            state: State filter ("unconfirmed" or "confirmed").

        Returns:
            List of shipment data dictionaries.
        """
        # Use pre-built queries with inline state values
        # (Letzshop's GraphQL has issues with enum variables)
        if state == "confirmed":
            query = QUERY_SHIPMENTS_CONFIRMED
        else:
            query = QUERY_SHIPMENTS_UNCONFIRMED

        logger.debug(f"Fetching shipments with state: {state}")
        data = self._execute(query)
        logger.debug(f"Shipments response data keys: {data.keys() if data else 'None'}")
        shipments_data = data.get("shipments", {})
        nodes = shipments_data.get("nodes", [])
        logger.info(f"Got {len(nodes)} {state} shipments from Letzshop API")
        return nodes

    def get_unconfirmed_shipments(self) -> list[dict[str, Any]]:
        """Get all unconfirmed shipments."""
        return self.get_shipments(state="unconfirmed")

    def get_shipment_by_id(self, shipment_id: str) -> dict[str, Any] | None:
        """
        Get a single shipment by its ID.

        Args:
            shipment_id: The Letzshop shipment ID.

        Returns:
            Shipment data or None if not found.
        """
        data = self._execute(QUERY_SHIPMENT_BY_ID, {"id": shipment_id})
        return data.get("node")

    def get_all_shipments_paginated(
        self,
        state: str = "confirmed",
        page_size: int = 50,
        max_pages: int | None = None,
        progress_callback: Callable[[int, int], None] | None = None,
    ) -> list[dict[str, Any]]:
        """
        Fetch all shipments with pagination support.

        Args:
            state: State filter ("unconfirmed" or "confirmed").
            page_size: Number of shipments per page (default 50).
            max_pages: Maximum number of pages to fetch (None = all).
            progress_callback: Optional callback(page, total_fetched) for progress updates.

        Returns:
            List of all shipment data dictionaries.
        """
        query = QUERY_SHIPMENTS_PAGINATED_TEMPLATE.format(state=state)
        all_shipments = []
        cursor = None
        page = 0

        while True:
            page += 1
            variables = {"first": page_size}
            if cursor:
                variables["after"] = cursor

            logger.info(f"Fetching {state} shipments page {page} (cursor: {cursor})")

            try:
                data = self._execute(query, variables)
            except LetzshopAPIError as e:
                # Log error but return what we have so far
                logger.error(f"Error fetching page {page}: {e}")
                break

            shipments_data = data.get("shipments", {})
            nodes = shipments_data.get("nodes", [])
            page_info = shipments_data.get("pageInfo", {})

            all_shipments.extend(nodes)

            if progress_callback:
                progress_callback(page, len(all_shipments))

            logger.info(f"Page {page}: fetched {len(nodes)} shipments, total: {len(all_shipments)}")

            # Check if there are more pages
            if not page_info.get("hasNextPage"):
                logger.info(f"Reached last page. Total shipments: {len(all_shipments)}")
                break

            cursor = page_info.get("endCursor")

            # Check max pages limit
            if max_pages and page >= max_pages:
                logger.info(f"Reached max pages limit ({max_pages}). Total shipments: {len(all_shipments)}")
                break

        return all_shipments

    # ========================================================================
    # Fulfillment Mutations
    # ========================================================================

    def confirm_inventory_units(
        self,
        inventory_unit_ids: list[str],
    ) -> dict[str, Any]:
        """
        Confirm inventory units for fulfillment.

        Args:
            inventory_unit_ids: List of inventory unit IDs to confirm.

        Returns:
            Response data including confirmed units and any errors.
        """
        variables = {
            "input": {
                "inventoryUnitIds": inventory_unit_ids,
            }
        }

        data = self._execute(MUTATION_CONFIRM_INVENTORY_UNITS, variables)
        return data.get("confirmInventoryUnits", {})

    def reject_inventory_units(
        self,
        inventory_unit_ids: list[str],
    ) -> dict[str, Any]:
        """
        Reject/return inventory units.

        Args:
            inventory_unit_ids: List of inventory unit IDs to reject.

        Returns:
            Response data including rejected units and any errors.
        """
        variables = {
            "input": {
                "inventoryUnitIds": inventory_unit_ids,
            }
        }

        data = self._execute(MUTATION_REJECT_INVENTORY_UNITS, variables)
        return data.get("returnInventoryUnits", {})

    def set_shipment_tracking(
        self,
        shipment_id: str,
        tracking_code: str,
        tracking_provider: str,
    ) -> dict[str, Any]:
        """
        Set tracking information for a shipment.

        Args:
            shipment_id: The Letzshop shipment ID.
            tracking_code: The tracking number.
            tracking_provider: The carrier code (e.g., "dhl", "ups").

        Returns:
            Response data including updated shipment and any errors.
        """
        variables = {
            "input": {
                "shipmentId": shipment_id,
                "tracking": {
                    "code": tracking_code,
                    "provider": tracking_provider,
                },
            }
        }

        data = self._execute(MUTATION_SET_SHIPMENT_TRACKING, variables)
        return data.get("setShipmentTracking", {})

    # ========================================================================
    # Store Directory Queries (Public - No Auth Required)
    # ========================================================================

    def get_all_stores_paginated(
        self,
        page_size: int = 50,
        max_pages: int | None = None,
        progress_callback: Callable[[int, int, int], None] | None = None,
    ) -> list[dict[str, Any]]:
        """
        Fetch all stores from Letzshop marketplace directory.

        This uses the public GraphQL API (no authentication required).

        Args:
            page_size: Number of stores per page (default 50).
            max_pages: Maximum number of pages to fetch (None = all).
            progress_callback: Optional callback(page, total_fetched, total_count)
                              for progress updates.

        Returns:
            List of all store data dictionaries.
        """
        all_stores = []
        cursor = None
        page = 0
        total_count = None

        while True:
            page += 1
            variables = {"first": page_size}
            if cursor:
                variables["after"] = cursor

            logger.info(f"Fetching stores page {page} (cursor: {cursor})")

            try:
                # Use public endpoint (no authentication required)
                data = self._execute_public(QUERY_STORES_PAGINATED, variables)
            except LetzshopAPIError as e:
                logger.error(f"Error fetching stores page {page}: {e}")
                break

            stores_data = data.get("stores", {})
            nodes = stores_data.get("nodes", [])
            page_info = stores_data.get("pageInfo", {})

            if total_count is None:
                total_count = stores_data.get("totalCount", 0)
                logger.info(f"Total stores in Letzshop: {total_count}")

            all_stores.extend(nodes)

            if progress_callback:
                progress_callback(page, len(all_stores), total_count)

            logger.info(
                f"Page {page}: fetched {len(nodes)} stores, "
                f"total: {len(all_stores)}/{total_count}"
            )

            # Check if there are more pages
            if not page_info.get("hasNextPage"):
                logger.info(f"Reached last page. Total stores: {len(all_stores)}")
                break

            cursor = page_info.get("endCursor")

            # Check max pages limit
            if max_pages and page >= max_pages:
                logger.info(
                    f"Reached max pages limit ({max_pages}). "
                    f"Total stores: {len(all_stores)}"
                )
                break

        return all_stores

    def get_store_by_slug(self, slug: str) -> dict[str, Any] | None:
        """
        Get a single store by their URL slug.

        Args:
            slug: The store's URL slug (e.g., "nicks-diecast-corner").

        Returns:
            Store data dictionary or None if not found.
        """
        try:
            # Use public endpoint (no authentication required)
            data = self._execute_public(QUERY_STORE_BY_SLUG, {"slug": slug})
            return data.get("store")
        except LetzshopAPIError as e:
            logger.warning(f"Store not found with slug '{slug}': {e}")
            return None
