"""
Async AltSportsData client — production-grade async data feed.

Uses httpx for async HTTP with connection pooling, HTTP/2, and
proper async context management.

    pip install altsportsdata[async]

    from altsportsdata import AsyncAltSportsData

    async with AsyncAltSportsData(api_key="key", odds_format="probability") as client:
        wsl = client.get_league("wsl")
        events = await wsl.list_events(status="upcoming")
        odds = await wsl.get_moneylines(events[0].id)

Requires: pip install httpx
"""

from typing import List, Optional, Dict, Any, Union, AsyncIterator

import asyncio
import logging
import random
import uuid

from .models import (
    SportType, OddType, EventStatus, FutureType,
    EventResponse, EventListing, EventParticipant, RoundScore,
    JaiAlaiEvent, JaiAlaiOdds,
    FutureListing, FutureOdds,
    SportsListing,
)
from .converters import OddsFormat, convert_response, convert_odds
from .exceptions import (
    raise_for_status, NetworkError, ConfigurationError,
    RateLimitError, RetryExhaustedError,
)

try:
    import httpx
except ImportError:
    httpx = None  # type: ignore

logger = logging.getLogger("altsportsdata.async")

__version__ = "3.0.0"

_BASE_URL = "https://api.altsportsdata.com"
_API_PREFIX = "/api/v1/public"
_RETRYABLE_STATUSES = {429, 500, 502, 503, 504}

# Import aliases from sync client
from .client import (
    _STATUS_ALIASES, _MARKET_ALIASES, _ALL_MARKET_TYPES,
    _enum_val, _resolve_status, _resolve_market,
)


class AsyncAltSportsData:
    """
    Async AltSportsData client — same API as the sync client, fully async.

    Uses httpx for async HTTP with connection pooling and HTTP/2 support.
    All methods are coroutines — use ``await`` to call them.

    Quick start::

        from altsportsdata import AsyncAltSportsData

        async with AsyncAltSportsData(api_key="key", odds_format="probability") as client:
            wsl = client.get_league("wsl")
            events = await wsl.list_events(status="upcoming")
            odds = await wsl.get_market_probabilities(events[0].id)
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        league: Optional[Union[SportType, str]] = None,
        *,
        base_url: str = _BASE_URL,
        timeout: int = 30,
        odds_format: Union[str, OddsFormat] = OddsFormat.DECIMAL,
        max_retries: int = 3,
        retry_backoff: float = 0.5,
    ):
        if httpx is None:
            raise ConfigurationError(
                "httpx required for async client. "
                "Install with: pip install altsportsdata[async]"
            )
        if not api_key:
            raise ConfigurationError(
                "API key required. Get one at https://altsportsdata.com"
            )

        self.api_key = api_key
        self.league = _enum_val(league, SportType) if league else None
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.odds_format = OddsFormat(odds_format) if isinstance(odds_format, str) else odds_format
        self.max_retries = max_retries
        self.retry_backoff = retry_backoff
        self._client: Optional[httpx.AsyncClient] = None

    def _ensure_client(self) -> "httpx.AsyncClient":
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                base_url=self.base_url,
                timeout=self.timeout,
                headers={
                    "X-API-KEY": self.api_key,
                    "Content-Type": "application/json",
                    "Accept": "application/json",
                    "User-Agent": f"altsportsdata-python-async/{__version__}",
                },
            )
        return self._client

    def get_league(self, league: Union[SportType, str]) -> "AsyncAltSportsData":
        """Get a league-scoped async client sharing the same connection."""
        child = AsyncAltSportsData.__new__(AsyncAltSportsData)
        child.api_key = self.api_key
        child.league = _enum_val(league, SportType)
        child.base_url = self.base_url
        child.timeout = self.timeout
        child.odds_format = self.odds_format
        child.max_retries = self.max_retries
        child.retry_backoff = self.retry_backoff
        child._client = self._client
        return child

    # ══════════════════════════════════════════════════════════════════════
    #  LEAGUES
    # ══════════════════════════════════════════════════════════════════════

    async def list_leagues(self) -> List[SportsListing]:
        data = await self._get(f"{_API_PREFIX}/sports")
        return [SportsListing.model_validate(i) for i in data] if isinstance(data, list) else []

    list_sports = list_leagues
    get_leagues = list_leagues

    async def get_league_info(self, league: Union[SportType, str]) -> Optional[SportsListing]:
        code = _enum_val(league, SportType)
        for lg in await self.list_leagues():
            if lg.league_key == code:
                return lg
        return None

    async def list_market_types(self) -> List[str]:
        all_types: set = set()
        for lg in await self.list_leagues():
            for m in lg.market_types:
                all_types.add(m)
        return sorted(all_types)

    # ══════════════════════════════════════════════════════════════════════
    #  EVENTS
    # ══════════════════════════════════════════════════════════════════════

    async def list_events(
        self,
        *,
        league: Optional[Union[SportType, str]] = None,
        status: Optional[Union[str, List[str], EventStatus, List[EventStatus]]] = None,
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
        sort: Optional[str] = None,
    ) -> List[EventListing]:
        params = self._build_event_params(
            league=league, status=status,
            start_date=start_date, end_date=end_date, sort=sort,
        )
        data = await self._get(f"{_API_PREFIX}/events", params=params)
        return [EventListing.model_validate(i) for i in data] if isinstance(data, list) else []

    get_events = list_events

    async def get_event(self, event_id: str) -> EventResponse:
        return EventResponse.model_validate(
            await self._get(f"{_API_PREFIX}/events/{event_id}")
        )

    async def get_participants(self, event_id: str) -> List[EventParticipant]:
        data = await self._get(f"{_API_PREFIX}/events/{event_id}/participants")
        return [EventParticipant.model_validate(i) for i in data] if isinstance(data, list) else []

    # ══════════════════════════════════════════════════════════════════════
    #  ODDS
    # ══════════════════════════════════════════════════════════════════════

    async def get_odds(
        self,
        event_id: str,
        market: Union[OddType, str],
        *,
        exactas_type: Optional[int] = None,
        sub_market: Optional[str] = None,
    ) -> Dict[str, Any]:
        market_str = _resolve_market(market)
        params: Dict[str, Any] = {}
        if exactas_type is not None:
            params["exactasType"] = exactas_type
        if sub_market is not None:
            params["overUnderSubMarketType"] = sub_market
        return await self._get(
            f"{_API_PREFIX}/events/{event_id}/{market_str}", params=params
        )

    async def get_odds_batch(
        self,
        event_ids: List[str],
        market: Union[OddType, str] = "eventWinner",
        *,
        max_concurrent: int = 5,
    ) -> Dict[str, Dict[str, Any]]:
        """Fetch odds for multiple events concurrently."""
        sem = asyncio.Semaphore(max_concurrent)

        async def _fetch(eid: str) -> tuple:
            async with sem:
                try:
                    return eid, await self.get_odds(eid, market)
                except Exception as e:
                    return eid, {"error": str(e)}

        tasks = [_fetch(eid) for eid in event_ids]
        results_list = await asyncio.gather(*tasks)
        return dict(results_list)

    # ── Shortcuts ──────────────────────────────────────────────────────

    async def get_market_probabilities(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "eventWinner")

    async def get_podium_probabilities(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "podiums")

    async def get_top_finish_probabilities(self, event_id: str, top_n: int = 5) -> Dict[str, Any]:
        market_map = {3: "raceTop3", 5: "raceTop5", 10: "raceTop10"}
        return await self.get_odds(event_id, market_map.get(top_n, f"raceTop{top_n}"))

    async def get_player_props(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "props")

    async def get_player_matchups(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "matchups")

    async def get_player_totals(self, event_id: str, stat: Optional[str] = None) -> Dict[str, Any]:
        return await self.get_odds(event_id, "totals", sub_market=stat)

    async def get_moneylines(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "moneyline")

    async def get_matchups(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "matchups")

    async def get_totals(self, event_id: str, sub_market: Optional[str] = None) -> Dict[str, Any]:
        return await self.get_odds(event_id, "totals", sub_market=sub_market)

    async def get_heat_winners(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "heatWinner")

    async def get_fastest_lap(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "fastestLap")

    async def get_podiums(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "podiums")

    async def get_shows(self, event_id: str) -> Dict[str, Any]:
        return await self.get_odds(event_id, "shows")

    # ══════════════════════════════════════════════════════════════════════
    #  SETTLEMENT
    # ══════════════════════════════════════════════════════════════════════

    async def get_settlement(
        self,
        event_id: str,
        *,
        markets: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """Structured settlement data — async version."""
        event = await self.get_event(event_id)
        market_types = markets or _ALL_MARKET_TYPES
        settled: Dict[str, Any] = {}

        # Fetch all market types concurrently
        async def _fetch_market(mtype: str):
            try:
                data = await self._get_raw(
                    f"{_API_PREFIX}/events/{event_id}/{mtype}"
                )
                return mtype, data
            except Exception:
                return mtype, None

        results = await asyncio.gather(*[_fetch_market(m) for m in market_types])

        for mtype, data in results:
            if data is None:
                continue
            extracted = self._extract_settlement(data, mtype)
            if extracted:
                settled[mtype] = extracted

        report = {
            "event_id": event_id,
            "event_name": event.name,
            "league": event.sportType,
            "status": event.eventStatus,
            "start_date": event.startDate,
            "end_date": event.endDate,
            "settled_at": event.endDate,
            "markets": settled,
            "markets_settled": len(settled),
        }
        if self.odds_format != OddsFormat.DECIMAL:
            report = convert_response(report, self.odds_format)
        return report

    def _extract_settlement(self, data: Dict, market_type: str) -> Optional[Dict]:
        """Same extraction logic as sync client."""
        if market_type in ("eventWinner", "secondPlace", "shows", "podiums",
                           "raceTop3", "raceTop5", "raceTop10", "fastestLap"):
            items = data.get(market_type, [])
            if not items:
                return None
            outcomes = []
            has_settlement = False
            for o in items:
                ath = o.get("athlete", {})
                entry = {
                    "athlete": f"{ath.get('firstName', '')} {ath.get('lastName', '')}".strip(),
                    "athlete_id": ath.get("id"),
                    "outcome_id": o.get("outcomeId") or o.get("id"),
                    "odds": o.get("odds"),
                    "probability": o.get("probability"),
                    "settlement": o.get("settlement"),
                    "status": o.get("status"),
                    "position": o.get("position"),
                    "updated_at": o.get("updatedAt"),
                }
                if o.get("settlement"):
                    has_settlement = True
                outcomes.append(entry)
            if not has_settlement:
                return None
            return {
                "outcomes": outcomes,
                "winners": [o for o in outcomes if o.get("settlement") == "WIN"],
                "total_outcomes": len(outcomes),
            }

        elif market_type == "headToHead":
            items = data.get("headToHead", [])
            if not items:
                return None
            matchups = []
            has_settlement = False
            for m in items:
                p1 = m.get("eventParticipant1", {})
                p2 = m.get("eventParticipant2", {})
                a1 = p1.get("athlete", {})
                a2 = p2.get("athlete", {})
                entry = {
                    "player1": f"{a1.get('firstName', '')} {a1.get('lastName', '')}".strip(),
                    "player1_odds": p1.get("odds"),
                    "player1_settlement": p1.get("settlement"),
                    "player2": f"{a2.get('firstName', '')} {a2.get('lastName', '')}".strip(),
                    "player2_odds": p2.get("odds"),
                    "player2_settlement": p2.get("settlement"),
                    "winner_id": m.get("winnerParticipantId"),
                }
                if p1.get("settlement") or p2.get("settlement"):
                    has_settlement = True
                matchups.append(entry)
            if not has_settlement:
                return None
            return {"matchups": matchups, "total_matchups": len(matchups)}

        return None

    # ══════════════════════════════════════════════════════════════════════
    #  LIVE POLLING
    # ══════════════════════════════════════════════════════════════════════

    async def poll_odds(
        self,
        event_id: str,
        market: Union[OddType, str] = "eventWinner",
        *,
        interval: float = 5.0,
        max_polls: Optional[int] = None,
    ) -> AsyncIterator[Dict[str, Any]]:
        """Async generator — polls odds at regular intervals."""
        import datetime
        poll_num = 0
        while max_polls is None or poll_num < max_polls:
            try:
                data = await self.get_odds(event_id, market)
                data["_polled_at"] = datetime.datetime.now(
                    datetime.timezone.utc
                ).isoformat()
                data["_poll_number"] = poll_num
                yield data
            except Exception as e:
                logger.warning(f"Poll {poll_num} failed: {e}")
                yield {"_error": str(e), "_poll_number": poll_num}
            poll_num += 1
            if max_polls is None or poll_num < max_polls:
                await asyncio.sleep(interval)

    # ══════════════════════════════════════════════════════════════════════
    #  FUTURES
    # ══════════════════════════════════════════════════════════════════════

    async def list_futures(self) -> List[FutureListing]:
        data = await self._get(f"{_API_PREFIX}/futures")
        return [FutureListing.model_validate(i) for i in data] if isinstance(data, list) else []

    async def get_futures(
        self,
        tour: str,
        type: Union[FutureType, str] = "winner",
        *,
        league: Optional[Union[SportType, str]] = None,
    ) -> List[FutureOdds]:
        league_val = _enum_val(league, SportType) if league else self.league
        if not league_val:
            raise ConfigurationError(
                "League required for futures. Set league= on client or pass league= here."
            )
        ft = _enum_val(type, FutureType)
        data = await self._get(f"{_API_PREFIX}/futures/{league_val}/tour/{tour}/odds/{ft}")
        return [FutureOdds.model_validate(i) for i in data] if isinstance(data, list) else []

    # ══════════════════════════════════════════════════════════════════════
    #  PARLAYS
    # ══════════════════════════════════════════════════════════════════════

    async def calculate_parlay(self, event_id: str, picks: List[str]) -> Dict[str, Any]:
        return await self._post(
            f"{_API_PREFIX}/events/{event_id}/sgp", body={"oddIds": picks}
        )

    calculate_sgp = calculate_parlay

    # ══════════════════════════════════════════════════════════════════════
    #  INTERNAL — async HTTP with retry
    # ══════════════════════════════════════════════════════════════════════

    def _build_event_params(self, league=None, status=None,
                            start_date=None, end_date=None, sort=None) -> Dict[str, Any]:
        params: Dict[str, Any] = {}
        lv = _enum_val(league, SportType) if league else self.league
        if lv:
            params["sportType"] = lv
        if status is not None:
            if isinstance(status, (str, EventStatus)):
                status = [status]
            params["eventStatus"] = [_resolve_status(s) for s in status]
        if start_date:
            params["startDate"] = start_date
        if end_date:
            params["endDate"] = end_date
        if sort:
            params["sortOrder"] = sort.upper()
        return params

    async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
        data = await self._request("GET", path, params=params)
        if self.odds_format != OddsFormat.DECIMAL:
            data = convert_response(data, self.odds_format)
        return data

    async def _get_raw(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
        return await self._request("GET", path, params=params)

    async def _post(self, path: str, body: Optional[Dict[str, Any]] = None) -> Any:
        data = await self._request("POST", path, body=body)
        if self.odds_format != OddsFormat.DECIMAL:
            data = convert_response(data, self.odds_format)
        return data

    async def _request(self, method: str, path: str, params=None, body=None) -> Any:
        client = self._ensure_client()
        request_id = str(uuid.uuid4())[:8]

        # Flatten list params
        req_params = None
        if params:
            flat = []
            for k, v in params.items():
                if isinstance(v, list):
                    for item in v:
                        flat.append((f"{k}[]", item))
                else:
                    flat.append((k, v))
            req_params = flat

        for attempt in range(self.max_retries + 1):
            try:
                r = await client.request(
                    method=method,
                    url=path,
                    params=req_params,
                    json=body,
                    headers={"X-Request-ID": f"asd-{request_id}-{attempt}"},
                )

                if r.is_success:
                    resp_body = r.json()
                    if isinstance(resp_body, dict) and "data" in resp_body and \
                            ("status" in resp_body or "statusCode" in resp_body):
                        return resp_body["data"]
                    return resp_body

                if r.status_code in _RETRYABLE_STATUSES and attempt < self.max_retries:
                    retry_after = r.headers.get("Retry-After")
                    delay = float(retry_after) if retry_after else (
                        self.retry_backoff * (2 ** attempt) + random.uniform(0, 0.5)
                    )
                    logger.info(
                        "Async request %s: HTTP %d, retry in %.1fs",
                        request_id, r.status_code, delay,
                    )
                    await asyncio.sleep(delay)
                    continue

                try:
                    msg = r.json().get("message", f"HTTP {r.status_code}")
                except Exception:
                    msg = f"HTTP {r.status_code}: {r.text}"

                if r.status_code in _RETRYABLE_STATUSES:
                    raise RetryExhaustedError(
                        f"All {self.max_retries + 1} attempts failed. Last: {msg}",
                        status_code=r.status_code,
                        attempts=attempt + 1,
                    )
                raise_for_status(r.status_code, msg)

            except httpx.TimeoutException:
                if attempt < self.max_retries:
                    delay = self.retry_backoff * (2 ** attempt) + random.uniform(0, 0.5)
                    await asyncio.sleep(delay)
                    continue
                raise NetworkError(
                    f"Request timed out ({self.max_retries + 1} attempts)"
                )

            except httpx.ConnectError:
                if attempt < self.max_retries:
                    delay = self.retry_backoff * (2 ** attempt) + random.uniform(0, 0.5)
                    await asyncio.sleep(delay)
                    continue
                raise NetworkError(
                    f"Connection failed ({self.max_retries + 1} attempts)"
                )

        raise NetworkError("Request failed after all retries")

    @staticmethod
    def convert(value: float, from_format: str = "decimal", to_format: str = "american"):
        return convert_odds(value, from_format, to_format)

    async def close(self) -> None:
        if self._client and not self._client.is_closed:
            await self._client.aclose()

    async def __aenter__(self) -> "AsyncAltSportsData":
        self._ensure_client()
        return self

    async def __aexit__(self, *args) -> None:
        await self.close()

    def __repr__(self) -> str:
        parts = [f"base_url='{self.base_url}'"]
        if self.league:
            parts.append(f"league='{self.league}'")
        if self.odds_format != OddsFormat.DECIMAL:
            parts.append(f"odds_format='{self.odds_format.value}'")
        return f"AsyncAltSportsData({', '.join(parts)})"
