"""TAB Affiliates API client with retry logic.

Handles retries, rate limiting, and error handling for the public TAB API.
No authentication required - this is a public API.
"""

from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Any

import httpx

from packages.core.common.logging import get_logger
from packages.core.common.settings import get_settings

logger = get_logger(__name__)

if TYPE_CHECKING:
    from packages.tab_client.mock_client import MockTABClient


class TABClientError(Exception):
    """Base exception for TAB client errors."""

    pass


class TABRateLimitError(TABClientError):
    """Rate limit exceeded."""

    pass


class TABClient:
    """Client for TAB Affiliates API with retry logic.

    The TAB Affiliates API is a public API that provides racing data
    for NZ and international racing. No authentication is required.

    Example:
        >>> async with TABClient() as client:
        >>>     meetings = await client.get_meetings("2024-01-01", "2024-01-07")
        >>>     for meeting in meetings:
        >>>         event_id = meeting["races"][0]["id"]
        >>>         event = await client.get_event(event_id)
    """

    def __init__(self):
        """Initialize TAB client with settings."""
        self.settings = get_settings()
        self.tab_config = self.settings.tab
        self._client: httpx.AsyncClient | None = None

    async def __aenter__(self):
        """Async context manager entry."""
        await self._ensure_client()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        await self.close()

    async def _ensure_client(self):
        """Ensure HTTP client is initialized."""
        if self._client is None:
            self._client = httpx.AsyncClient(
                timeout=self.tab_config.timeout,
                follow_redirects=True,
            )

    async def close(self):
        """Close HTTP client."""
        if self._client is not None:
            await self._client.aclose()
            self._client = None

    async def _request_with_retry(
        self,
        method: str,
        endpoint: str,
        params: dict[str, Any] | None = None,
        retry_count: int = 0,
    ) -> dict[str, Any]:
        """Make HTTP request with retry logic.

        Args:
            method: HTTP method (GET, POST, etc.)
            endpoint: API endpoint path (without base URL)
            params: Query parameters
            retry_count: Current retry attempt

        Returns:
            Response JSON data

        Raises:
            TABClientError: If request fails after retries
        """
        await self._ensure_client()

        url = f"{self.tab_config.base_url}/{endpoint}"

        # Debug logging
        logger.debug(
            f"Making {method} request to {url}",
            extra={
                "endpoint": endpoint,
                "params": params,
            },
        )

        try:
            response = await self._client.request(method, url, params=params)

            logger.debug(
                f"Response status: {response.status_code}",
                extra={"status_code": response.status_code, "endpoint": endpoint},
            )

            # Handle rate limiting
            if response.status_code == 429:
                if retry_count >= self.tab_config.max_retries:
                    raise TABRateLimitError("Rate limit exceeded, max retries reached")

                # Exponential backoff
                wait_time = 2**retry_count
                logger.warning(f"Rate limited, waiting {wait_time}s before retry")
                await asyncio.sleep(wait_time)
                return await self._request_with_retry(
                    method, endpoint, params, retry_count + 1
                )

            # Handle client errors
            if response.status_code == 400:
                try:
                    error_body = response.json()
                    error_msg = error_body.get("error", response.text)
                except Exception:
                    error_msg = response.text
                raise TABClientError(f"Bad request: {error_msg}")

            # Handle not found
            if response.status_code == 404:
                raise TABClientError(f"Resource not found: {endpoint}")

            response.raise_for_status()
            return response.json()

        except httpx.HTTPStatusError as e:
            if retry_count >= self.tab_config.max_retries:
                raise TABClientError(f"HTTP error: {e}") from e

            # Retry on 5xx errors
            if 500 <= e.response.status_code < 600:
                wait_time = 2**retry_count
                logger.warning(
                    f"Server error {e.response.status_code}, "
                    f"waiting {wait_time}s before retry"
                )
                await asyncio.sleep(wait_time)
                return await self._request_with_retry(
                    method, endpoint, params, retry_count + 1
                )

            raise TABClientError(f"HTTP error: {e}") from e

        except httpx.RequestError as e:
            if retry_count >= self.tab_config.max_retries:
                raise TABClientError(f"Request error: {e}") from e

            wait_time = 2**retry_count
            logger.warning(f"Request failed, waiting {wait_time}s before retry")
            await asyncio.sleep(wait_time)
            return await self._request_with_retry(
                method, endpoint, params, retry_count + 1
            )

    async def get_meetings(
        self,
        date_from: str,
        date_to: str,
        category: str | None = None,
        country: str | None = None,
    ) -> list[dict[str, Any]]:
        """Fetch meetings within date range.

        Args:
            date_from: Start date (YYYY-MM-DD)
            date_to: End date (YYYY-MM-DD)
            category: Racing category filter - "T" (Thoroughbred),
                      "H" (Harness), "G" (Greyhound). If None, uses config default.
            country: Country filter (e.g., "NZ", "AUS"). If None, uses config default.

        Returns:
            List of meeting data dictionaries

        Example:
            >>> async with TABClient() as client:
            >>>     meetings = await client.get_meetings(
            >>>         "2024-01-01", "2024-01-07", category="H", country="NZ"
            >>>     )
        """
        # Use config defaults if not specified
        if category is None:
            category = self.tab_config.default_category
        if country is None:
            country = self.tab_config.default_country

        logger.info(
            f"Fetching {category} meetings from {date_from} to {date_to} "
            f"in {country}"
        )

        params = {
            "date_from": date_from,
            "date_to": date_to,
            "category": category,
            "country": country,
        }

        response = await self._request_with_retry("GET", "racing/meetings", params)

        # TAB API returns {"meetings": [...]}
        meetings = response.get("meetings", [])
        logger.info(f"Found {len(meetings)} meetings")
        return meetings

    async def get_meeting(self, meeting_id: str) -> dict[str, Any]:
        """Fetch single meeting details with race summaries.

        Args:
            meeting_id: TAB meeting ID (string)

        Returns:
            Meeting data dictionary with 'races' array (summaries only)

        Note:
            The races in this response only contain summary info (id, race_number).
            Use get_event() to fetch full race details with runners.
        """
        logger.info(f"Fetching meeting {meeting_id}")

        response = await self._request_with_retry(
            "GET", f"racing/meetings/{meeting_id}"
        )

        # TAB API returns {"meetings": [single_meeting]}
        meetings = response.get("meetings", [])
        if not meetings:
            raise TABClientError(f"Meeting not found: {meeting_id}")

        return meetings[0]

    async def get_event(self, event_id: str) -> dict[str, Any]:
        """Fetch race/event details with runners and results.

        Args:
            event_id: TAB event ID (string)

        Returns:
            Event data dictionary with:
                - race: Race information (distance, start_type, etc.)
                - runners: List of runners with horse, driver, trainer info
                - results: List of results with positions (if race is finished)
                - dividends: Dividend information (if available)

        Example:
            >>> async with TABClient() as client:
            >>>     event = await client.get_event("abc123")
            >>>     runners = event.get("runners", [])
            >>>     results = event.get("results", [])
        """
        logger.info(f"Fetching event {event_id}")

        response = await self._request_with_retry("GET", f"racing/events/{event_id}")

        # TAB API wraps event data in a "data" key
        # Response: {"data": {"race": {...}, "runners": [...], "results": [...]}}
        data = response.get("data", {})

        if not data:
            logger.warning(f"Event {event_id} returned empty data")

        return data

    async def get_races_list(
        self,
        date_from: str,
        date_to: str,
        meet_types: str | None = None,
        countries: str | None = None,
        limit: int = 100,
    ) -> dict[str, Any]:
        """Fetch list of races with pagination.

        This is an alternative to get_meetings() that returns races directly
        with more filtering options.

        Args:
            date_from: Start date (YYYY-MM-DD)
            date_to: End date (YYYY-MM-DD)
            meet_types: Racing type filter - "T", "H", "G" (can be combined)
            countries: Country filter - "NZ", "AUS", etc.
            limit: Maximum results per page (max 200)

        Returns:
            Dictionary with:
                - races: List of race data
                - page_token: Token for next page (if more results)

        Note:
            For paginated results, use page_token in subsequent requests.
        """
        if meet_types is None:
            meet_types = self.tab_config.default_category
        if countries is None:
            countries = self.tab_config.default_country

        logger.info(
            f"Fetching race list from {date_from} to {date_to} "
            f"(types={meet_types}, countries={countries})"
        )

        params = {
            "date_from": date_from,
            "date_to": date_to,
            "meet_types": meet_types,
            "countries": countries,
            "limit": min(limit, 200),  # API max is 200
        }

        response = await self._request_with_retry("GET", "racing/list", params)
        return response


def get_client() -> TABClient | MockTABClient:
    """Get TAB client (real or mock based on settings).

    Returns:
        TABClient or MockTABClient instance

    Example:
        >>> async with get_client() as client:
        >>>     meetings = await client.get_meetings("2024-01-01", "2024-01-07")
    """
    settings = get_settings()

    if settings.tab.mock_mode:
        from packages.tab_client.mock_client import MockTABClient

        logger.info("Using MockTABClient (TAB_MOCK_MODE=true)")
        return MockTABClient()
    else:
        return TABClient()
