"""Connexity gateway API helpers.

This module provides small utilities used by the SDK to serialize payloads
(e.g., UUIDs) and send them to the Connexity gateway over HTTP.
"""

import asyncio
import json
import random
from typing import Any
from uuid import UUID

import aiohttp

from connexity.calls.models import CallSessionData
from connexity.constants import CONNEXITY_URL
from connexity.utils.logging_config import get_logger

logger = get_logger(__name__)

# HTTP status codes that should trigger retries
RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}

# Maximum number of retry attempts
MAX_RETRIES = 3

# Exponential backoff configuration
BACKOFF_MULTIPLIER = 1
BACKOFF_MIN_SECONDS = 2
BACKOFF_MAX_SECONDS = 10


def convert_uuids(obj: Any) -> Any:
    """Recursively convert UUID objects to strings.

    This is useful before JSON-encoding payloads, since UUID is not JSON
    serializable by default.

    Args:
        obj: An arbitrary object that may contain UUID instances.

    Returns:
        The same structure as `obj`, but with UUID instances converted to
        their string representation.
    """
    if isinstance(obj, UUID):
        return str(obj)

    if isinstance(obj, dict):
        return {k: convert_uuids(v) for k, v in obj.items()}

    if isinstance(obj, list):
        return [convert_uuids(item) for item in obj]

    return obj


def _parse_retry_after(header_value: str | None) -> float | None:
    """Parse Retry-After header value to seconds.

    Supports both numeric seconds and HTTP-date formats, but only returns
    numeric seconds for simplicity. Returns None if parsing fails.

    Args:
        header_value: The Retry-After header value.

    Returns:
        Number of seconds to wait, or None if parsing fails.
    """
    if not header_value:
        return None

    try:
        # Try parsing as numeric seconds first
        return float(header_value)
    except ValueError:
        # Could implement HTTP-date parsing here if needed
        return None


async def _send_request_with_retry(
    url: str,
    headers: dict[str, str],
    json_data: dict[str, Any],
    timeout: aiohttp.ClientTimeout,
) -> tuple[int, str]:
    """Internal function to send HTTP request with retry logic.

    Implements exponential backoff with jitter and respects Retry-After headers
    for rate limiting (429) and service unavailable (503) responses.

    Args:
        url: Gateway URL to send request to.
        headers: HTTP headers including authentication.
        json_data: JSON payload to send.
        timeout: Request timeout configuration.

    Returns:
        Tuple of (status_code, response_body_text).

    Raises:
        aiohttp.ClientError: For network-related errors after all retries exhausted.
        TimeoutError: If request times out after all retries exhausted.
    """
    last_exception: Exception | None = None

    async with aiohttp.ClientSession(timeout=timeout) as session:
        for attempt in range(MAX_RETRIES):
            try:
                async with session.post(url, headers=headers, json=json_data) as response:
                    status = response.status
                    # Read response body while response is still valid
                    try:
                        body_text = await response.text()
                    except Exception:
                        body_text = ""

                    # Success - return immediately
                    if status == 200:
                        return (status, body_text)

                    # Check if status is retryable
                    if status in RETRYABLE_STATUS_CODES:
                        # Check for Retry-After header (429, 503)
                        retry_after: float | None = None
                        if status in {429, 503}:
                            retry_after = _parse_retry_after(
                                response.headers.get("Retry-After")
                            )

                        # Calculate backoff delay
                        if retry_after is not None:
                            delay = retry_after
                        else:
                            # Exponential backoff: multiplier * (2^attempt)
                            base_delay = BACKOFF_MULTIPLIER * (2**attempt)
                            # Clamp to min/max bounds
                            delay = max(
                                BACKOFF_MIN_SECONDS,
                                min(base_delay, BACKOFF_MAX_SECONDS),
                            )
                            # Add small jitter (±10%)
                            jitter = delay * 0.1 * (2 * random.random() - 1)
                            delay = delay + jitter

                        # Don't retry on last attempt
                        if attempt < MAX_RETRIES - 1:
                            logger.debug(
                                f"Retryable status {status} received, "
                                f"retrying in {delay:.2f}s (attempt {attempt + 1}/{MAX_RETRIES})"
                            )
                            await asyncio.sleep(delay)
                            continue

                    # Non-retryable status or last attempt - return status and body
                    return (status, body_text)

            except (aiohttp.ClientError, TimeoutError) as e:
                last_exception = e
                # Don't retry on last attempt
                if attempt < MAX_RETRIES - 1:
                    # Exponential backoff with jitter
                    base_delay = BACKOFF_MULTIPLIER * (2**attempt)
                    delay = max(
                        BACKOFF_MIN_SECONDS,
                        min(base_delay, BACKOFF_MAX_SECONDS),
                    )
                    jitter = delay * 0.1 * (2 * random.random() - 1)
                    delay = delay + jitter

                    logger.debug(
                        f"Network error occurred: {e}, "
                        f"retrying in {delay:.2f}s (attempt {attempt + 1}/{MAX_RETRIES})"
                    )
                    await asyncio.sleep(delay)
                else:
                    # Last attempt failed - re-raise
                    raise

    # Should never reach here, but satisfy type checker
    if last_exception:
        raise last_exception
    raise RuntimeError("Unexpected retry loop exit")


async def send_to_gateway(
    data: CallSessionData | dict[str, Any], api_key: str, url: str = CONNEXITY_URL
) -> None:
    """Send call-session data to Connexity's gateway API.

    This function includes retry logic with exponential backoff and an extended
    timeout to handle transient network issues and slow responses. It retries on:
    - Network errors (aiohttp.ClientError, TimeoutError)
    - Retryable HTTP status codes (408, 429, 500, 502, 503, 504)
    - Respects Retry-After headers for rate limiting

    Args:
        data: A `CallSessionData` model or a dict payload to send.
        api_key: API key used for the `X-API-KEY` authentication header.
        url: Gateway URL (defaults to `CONNEXITY_URL`).

    Returns:
        None
    """
    try:
        if isinstance(data, CallSessionData):
            data = data.model_dump()
        # Ensure UUID values are JSON-serializable before sending.
        converted_data = convert_uuids(data)

        # Log the JSON payload being sent to the gateway
        logger.debug(f"Sending JSON payload to Connexity gateway: {json.dumps(converted_data, indent=2)}")

        # Extended timeout to handle slower responses
        timeout = aiohttp.ClientTimeout(total=25)

        status, body = await _send_request_with_retry(
            url=url,
            headers={"X-API-KEY": api_key},
            json_data=converted_data,
            timeout=timeout,
        )

        if status == 200:
            logger.info("Data sent successfully to Connexity gateway (status 200)")
        else:
            logger.error(
                f"Failed to send data to Connexity gateway: "
                f"status={status}, error={body[:500] if body else '(empty response)'}"
            )

    except TimeoutError as e:
        logger.error(f"Failed to send data to Connexity gateway: timeout error - {e}")

    except aiohttp.ClientError as e:
        logger.error(f"Failed to send data to Connexity gateway: network error - {e}")

    except Exception as e:
        logger.error(
            f"Failed to send data to Connexity gateway: unexpected error - {e}", exc_info=True
        )
