"""Kalshi API Client — synchronous."""

from __future__ import annotations

import json
import logging
import time
from functools import cached_property
from typing import Any
from urllib.parse import urlencode

import httpx

from ._base import _BaseKalshiClient, _RETRYABLE_STATUS_CODES
from .events import Event
from .markets import Market, Series
from .mve import MveCollection
from .models import MarketModel, EventModel, SeriesModel, TradeModel, CandlestickResponse, MveCollectionModel
from .dataframe import DataFrameList
from .portfolio import Portfolio
from .enums import MarketStatus, CandlestickPeriod
from .feed import Feed
from .exchange import Exchange
from .api_keys import APIKeys
from .communications import Communications
from .rate_limiter import RateLimiterProtocol
from .exceptions import RateLimitError
from ._utils import normalize_ticker, normalize_tickers

logger = logging.getLogger(__name__)


class KalshiClient(_BaseKalshiClient):
    """Authenticated client for the Kalshi Trading API.

    Usage:
        client = KalshiClient.from_env()  # Loads .env file
        client = KalshiClient(api_key_id="...", private_key_path="...")
    """

    def __init__(
        self,
        api_key_id: str | None = None,
        private_key_path: str | None = None,
        api_base: str | None = None,
        demo: bool = False,
        timeout: float = 10.0,
        max_retries: int = 3,
        rate_limiter: RateLimiterProtocol | None = None,
    ) -> None:
        super().__init__(
            api_key_id=api_key_id,
            private_key_path=private_key_path,
            api_base=api_base,
            demo=demo,
            timeout=timeout,
            max_retries=max_retries,
            rate_limiter=rate_limiter,
        )
        self._session = httpx.Client()

    def close(self) -> None:
        """Close the underlying HTTP connection pool."""
        self._session.close()

    def __enter__(self) -> KalshiClient:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    # --- HTTP methods ---

    def _request(self, method: str, endpoint: str, **kwargs: Any) -> httpx.Response:
        """Execute HTTP request with retry on transient failures."""
        url = f"{self.api_base}{endpoint}"

        for attempt in range(self.max_retries + 1):
            if self.rate_limiter is not None:
                wait_time = self.rate_limiter.acquire()
                if wait_time > 0:
                    logger.debug("Rate limiter waited %.3fs", wait_time)

            headers = self._get_headers(method, endpoint)
            request_kwargs: dict[str, Any] = {"headers": headers, "timeout": self.timeout}
            if "data" in kwargs:
                request_kwargs["content"] = kwargs["data"]
            try:
                response = self._session.request(method, url, **request_kwargs)
            except httpx.TimeoutException as e:
                if attempt == self.max_retries:
                    raise
                wait = self._compute_backoff(attempt, None)
                logger.warning(
                    "%s %s failed (%s), retry %d/%d in %.1fs",
                    method, endpoint, type(e).__name__,
                    attempt + 1, self.max_retries, wait,
                )
                time.sleep(wait)
                continue
            except httpx.ConnectError as e:
                if attempt == self.max_retries:
                    raise
                wait = self._compute_backoff(attempt, None)
                logger.warning(
                    "%s %s failed (%s), retry %d/%d in %.1fs",
                    method, endpoint, type(e).__name__,
                    attempt + 1, self.max_retries, wait,
                )
                time.sleep(wait)
                continue

            self._update_rate_limiter(response)

            if response.status_code not in _RETRYABLE_STATUS_CODES:
                return response
            if attempt == self.max_retries:
                if response.status_code == 429:
                    raise RateLimitError(
                        429, "Rate limit exceeded after retries",
                        method=method, endpoint=endpoint,
                    )
                return response

            wait = self._compute_backoff(attempt, response.headers.get("Retry-After"))
            logger.warning(
                "%s %s returned %d, retry %d/%d in %.1fs",
                method, endpoint, response.status_code,
                attempt + 1, self.max_retries, wait,
            )
            time.sleep(wait)

        return response  # unreachable, satisfies type checker

    def get(self, endpoint: str) -> dict[str, Any]:
        """Make authenticated GET request."""
        logger.debug("GET %s", endpoint)
        response = self._request("GET", endpoint)
        return self._handle_response(response, method="GET", endpoint=endpoint)

    def paginated_get(
        self,
        path: str,
        response_key: str,
        params: dict[str, Any],
        fetch_all: bool = False,
    ) -> list[dict]:
        """Fetch items with automatic cursor-based pagination."""
        params = dict(params)
        all_items: list[dict] = []
        while True:
            filtered = {k: v for k, v in params.items() if v is not None}
            endpoint = f"{path}?{urlencode(filtered)}" if filtered else path
            response = self.get(endpoint)
            all_items.extend(response.get(response_key, []))
            cursor = response.get("cursor", "")
            if not fetch_all or not cursor:
                break
            params["cursor"] = cursor
        return all_items

    def post(self, endpoint: str, data: dict[str, Any]) -> dict[str, Any]:
        """Make authenticated POST request."""
        logger.debug("POST %s", endpoint)
        body = json.dumps(data, separators=(",", ":"))
        response = self._request("POST", endpoint, data=body)
        return self._handle_response(
            response, method="POST", endpoint=endpoint, request_body=data
        )

    def put(self, endpoint: str, data: dict[str, Any]) -> dict[str, Any]:
        """Make authenticated PUT request."""
        logger.debug("PUT %s", endpoint)
        body = json.dumps(data, separators=(",", ":"))
        response = self._request("PUT", endpoint, data=body)
        return self._handle_response(
            response, method="PUT", endpoint=endpoint, request_body=data
        )

    def delete(self, endpoint: str, body: dict | None = None) -> dict[str, Any]:
        """Make authenticated DELETE request."""
        logger.debug("DELETE %s", endpoint)
        if body:
            data = json.dumps(body, separators=(",", ":"))
            response = self._request("DELETE", endpoint, data=data)
        else:
            response = self._request("DELETE", endpoint)
        return self._handle_response(response, method="DELETE", endpoint=endpoint)

    # --- Domain accessors ---

    @cached_property
    def portfolio(self) -> Portfolio:
        """The authenticated user's portfolio."""
        return Portfolio(self)

    @cached_property
    def exchange(self) -> Exchange:
        """Exchange status, schedule, and announcements."""
        return Exchange(self)

    @cached_property
    def api_keys(self) -> APIKeys:
        """API key management and rate limits."""
        return APIKeys(self)

    @cached_property
    def communications(self) -> Communications:
        """RFQ and quote operations for combo (multivariate event) trading."""
        return Communications(self)

    def feed(self) -> Feed:
        """Create a new real-time data feed.

        Returns a Feed instance for streaming market data via WebSocket.
        Each call creates a new Feed — use a single Feed for all subscriptions.
        """
        return Feed(self)

    # --- Domain query methods ---

    def get_market(self, ticker: str) -> Market:
        """Get a Market by ticker."""
        response = self.get(f"/markets/{ticker.upper()}")
        model = MarketModel.model_validate(response["market"])
        return Market(self, model)

    def get_markets(
        self,
        *,
        status: MarketStatus | None = None,
        mve_filter: str | None = None,
        tickers: list[str] | None = None,
        series_ticker: str | None = None,
        event_ticker: str | None = None,
        limit: int = 100,
        cursor: str | None = None,
        fetch_all: bool = False,
        **extra_params,
    ) -> DataFrameList[Market]:
        """Search for markets."""
        params = {
            "status": status.value if status is not None else None,
            "mve_filter": mve_filter,
            "tickers": ",".join(normalize_tickers(tickers)) if tickers else None,
            "series_ticker": normalize_ticker(series_ticker),
            "event_ticker": normalize_ticker(event_ticker),
            "limit": limit,
            "cursor": cursor,
            **extra_params,
        }
        data = self.paginated_get("/markets", "markets", params, fetch_all)
        return DataFrameList(Market(self, MarketModel.model_validate(m)) for m in data)

    def get_event(
        self,
        event_ticker: str,
        *,
        with_nested_markets: bool = False,
    ) -> Event:
        """Get an Event by ticker."""
        params = {}
        if with_nested_markets:
            params["with_nested_markets"] = "true"
        endpoint = f"/events/{event_ticker.upper()}"
        if params:
            endpoint += "?" + urlencode(params)
        response = self.get(endpoint)
        model = EventModel.model_validate(response["event"])
        return Event(self, model)

    def get_events(
        self,
        *,
        series_ticker: str | None = None,
        status: MarketStatus | None = None,
        limit: int = 100,
        cursor: str | None = None,
        fetch_all: bool = False,
        **extra_params,
    ) -> DataFrameList[Event]:
        """Search for events."""
        params = {
            "limit": limit,
            "series_ticker": normalize_ticker(series_ticker),
            "status": status.value if status is not None else None,
            "cursor": cursor,
            **extra_params,
        }
        data = self.paginated_get("/events", "events", params, fetch_all)
        return DataFrameList(Event(self, EventModel.model_validate(e)) for e in data)

    def get_series(
        self,
        series_ticker: str,
        *,
        include_volume: bool = False,
    ) -> Series:
        """Get a Series by ticker."""
        params = {}
        if include_volume:
            params["include_volume"] = "true"
        endpoint = f"/series/{series_ticker.upper()}"
        if params:
            endpoint += "?" + urlencode(params)
        response = self.get(endpoint)
        model = SeriesModel.model_validate(response["series"])
        return Series(self, model)

    def get_all_series(
        self,
        *,
        category: str | None = None,
        limit: int = 100,
        cursor: str | None = None,
        fetch_all: bool = False,
        **extra_params,
    ) -> DataFrameList[Series]:
        """List all series."""
        params = {"limit": limit, "category": category, "cursor": cursor, **extra_params}
        data = self.paginated_get("/series", "series", params, fetch_all)
        return DataFrameList(Series(self, SeriesModel.model_validate(s)) for s in data)

    def get_mve_collection(self, collection_ticker: str) -> MveCollection:
        """Get a multivariate event collection by ticker."""
        response = self.get(f"/multivariate_event_collections/{collection_ticker}")
        model = MveCollectionModel.model_validate(response.get("multivariate_contract", response))
        return MveCollection(self, model)

    def get_mve_collections(
        self,
        *,
        status: str | None = None,
        associated_event_ticker: str | None = None,
        series_ticker: str | None = None,
        limit: int = 100,
        cursor: str | None = None,
        fetch_all: bool = False,
    ) -> DataFrameList[MveCollection]:
        """List multivariate event collections."""
        params = {
            "limit": limit,
            "status": status,
            "associated_event_ticker": normalize_ticker(associated_event_ticker),
            "series_ticker": normalize_ticker(series_ticker),
            "cursor": cursor,
        }
        data = self.paginated_get(
            "/multivariate_event_collections", "multivariate_contracts", params, fetch_all
        )
        return DataFrameList(
            MveCollection(self, MveCollectionModel.model_validate(c)) for c in data
        )

    def get_multivariate_events(
        self,
        *,
        series_ticker: str | None = None,
        collection_ticker: str | None = None,
        with_nested_markets: bool = False,
        limit: int = 200,
        cursor: str | None = None,
        fetch_all: bool = False,
    ) -> DataFrameList[Event]:
        """Get multivariate (combo) events."""
        params: dict = {"limit": limit}
        if series_ticker:
            params["series_ticker"] = normalize_ticker(series_ticker)
        if collection_ticker:
            params["collection_ticker"] = collection_ticker
        if with_nested_markets:
            params["with_nested_markets"] = "true"
        if cursor:
            params["cursor"] = cursor

        data = self.paginated_get("/events/multivariate", "events", params, fetch_all)
        return DataFrameList(Event(self, EventModel.model_validate(e)) for e in data)

    def get_trades(
        self,
        *,
        ticker: str | None = None,
        min_ts: int | None = None,
        max_ts: int | None = None,
        limit: int = 100,
        cursor: str | None = None,
        fetch_all: bool = False,
        **extra_params,
    ) -> DataFrameList[TradeModel]:
        """Get public trade history."""
        params = {
            "limit": limit,
            "ticker": normalize_ticker(ticker),
            "min_ts": min_ts,
            "max_ts": max_ts,
            "cursor": cursor,
            **extra_params,
        }
        data = self.paginated_get("/markets/trades", "trades", params, fetch_all)
        return DataFrameList(TradeModel.model_validate(t) for t in data)

    def get_candlesticks_batch(
        self,
        tickers: list[str],
        start_ts: int,
        end_ts: int,
        period: CandlestickPeriod = CandlestickPeriod.ONE_HOUR,
    ) -> dict[str, CandlestickResponse]:
        """Batch fetch candlesticks for multiple markets (up to 100 tickers)."""
        query = urlencode({
            "market_tickers": ",".join(normalize_tickers(tickers)),
            "start_ts": start_ts,
            "end_ts": end_ts,
            "period_interval": period.value,
        })
        response = self.get(f"/markets/candlesticks?{query}")
        return {
            item["market_ticker"]: CandlestickResponse.model_validate(item)
            for item in response.get("markets", [])
        }
