from __future__ import annotations

import time
from collections.abc import Callable
from decimal import Decimal
from typing import Any
from urllib.parse import urlencode

import httpx

from .audit_log import AuditLogger
from .exchange import sign_query
from .futures_models import FuturesOrderSide, FuturesPosition, FuturesSymbolMetadata


FUTURES_TESTNET_REST_BASE_URL = "https://testnet.binancefuture.com"
RETRYABLE_HTTP_STATUS_CODES = {301, 302, 307, 308}


class BinanceUsdmFuturesClient:
    def __init__(
        self,
        api_key: str,
        api_secret: str,
        base_url: str = FUTURES_TESTNET_REST_BASE_URL,
        base_urls: tuple[str, ...] | None = None,
        http_client: httpx.Client | None = None,
        recv_window: int = 5000,
        proxy_url: str | None = None,
        audit_logger: AuditLogger | None = None,
        clock: Callable[[], float] | None = None,
    ) -> None:
        self.api_key = api_key
        self.api_secret = api_secret
        self.base_urls = tuple(item.rstrip("/") for item in (base_urls or (base_url,)))
        self.base_url = self.base_urls[0]
        self.recv_window = recv_window
        self._client = http_client or httpx.Client(proxy=proxy_url, timeout=10, trust_env=False)
        self.audit_logger = audit_logger or AuditLogger()
        self._clock = clock or time.time
        self._server_time_offset_ms: int | None = None

    def exchange_info(self, symbol: str) -> FuturesSymbolMetadata:
        requested_symbol = symbol.upper()
        payload = self._public_request("GET", "/fapi/v1/exchangeInfo", {"symbol": requested_symbol})
        symbol_info = next(item for item in payload["symbols"] if item["symbol"].upper() == requested_symbol)
        filters = {item["filterType"]: item for item in symbol_info["filters"]}
        lot_size = filters.get("LOT_SIZE", {})
        price_filter = filters.get("PRICE_FILTER", {})
        min_notional_filter = filters.get("MIN_NOTIONAL") or filters.get("NOTIONAL") or {}
        return FuturesSymbolMetadata(
            symbol=symbol_info["symbol"],
            base_asset=symbol_info["baseAsset"],
            quote_asset=symbol_info["quoteAsset"],
            min_qty=Decimal(lot_size.get("minQty", "0")),
            step_size=Decimal(lot_size.get("stepSize", "0")),
            tick_size=Decimal(price_filter.get("tickSize", "0")),
            min_notional=Decimal(min_notional_filter.get("notional", min_notional_filter.get("minNotional", "0"))),
        )

    def klines(self, symbol: str, interval: str, limit: int) -> list:
        return self._public_request(
            "GET",
            "/fapi/v1/klines",
            {"symbol": symbol.upper(), "interval": interval, "limit": str(limit)},
        )

    def depth(self, symbol: str, limit: int = 20) -> dict:
        return self._public_request("GET", "/fapi/v1/depth", {"symbol": symbol.upper(), "limit": str(limit)})

    def ticker_24hr(self, symbol: str) -> dict:
        return self._public_request("GET", "/fapi/v1/ticker/24hr", {"symbol": symbol.upper()})

    def aggregate_trades(self, symbol: str, limit: int = 500) -> list:
        return self._public_request("GET", "/fapi/v1/aggTrades", {"symbol": symbol.upper(), "limit": str(limit)})

    def mark_price(self, symbol: str) -> dict:
        return self._public_request("GET", "/fapi/v1/premiumIndex", {"symbol": symbol.upper()})

    def open_interest(self, symbol: str) -> dict:
        return self._public_request("GET", "/fapi/v1/openInterest", {"symbol": symbol.upper()})

    def global_long_short_ratio(self, symbol: str, period: str = "5m", limit: int = 1) -> list:
        return self._public_request(
            "GET",
            "/futures/data/globalLongShortAccountRatio",
            {"symbol": symbol.upper(), "period": period, "limit": str(limit)},
        )

    def taker_long_short_ratio(self, symbol: str, period: str = "5m", limit: int = 1) -> list:
        return self._public_request(
            "GET",
            "/futures/data/takerlongshortRatio",
            {"symbol": symbol.upper(), "period": period, "limit": str(limit)},
        )

    def open_interest_hist(self, symbol: str, period: str = "15m", limit: int = 12) -> list:
        payload = self._public_request(
            "GET",
            "/futures/data/openInterestHist",
            {"symbol": symbol.upper(), "period": period, "limit": str(limit)},
        )
        if not isinstance(payload, list):
            raise ValueError("open interest history response must be a list")
        return payload

    def funding_rate_history(self, symbol: str, limit: int = 8) -> list:
        payload = self._public_request(
            "GET",
            "/fapi/v1/fundingRate",
            {"symbol": symbol.upper(), "limit": str(limit)},
        )
        if not isinstance(payload, list):
            raise ValueError("funding rate history response must be a list")
        return payload

    def current_position_mode(self, timestamp_ms: int | None = None) -> bool:
        payload = self._signed_request("GET", "/fapi/v1/positionSide/dual", {}, timestamp_ms)
        return bool(payload["dualSidePosition"])

    def account(self, timestamp_ms: int | None = None) -> dict:
        payload = self._signed_request("GET", "/fapi/v3/account", {}, timestamp_ms)
        if not isinstance(payload, dict):
            raise ValueError("account response must be a dict")
        return payload

    def position_risk(self, symbol: str | None = None, timestamp_ms: int | None = None) -> list[dict]:
        params = {"symbol": symbol.upper()} if symbol else {}
        payload = self._signed_request("GET", "/fapi/v2/positionRisk", params, timestamp_ms)
        if not isinstance(payload, list):
            raise ValueError("position risk response must be a list")
        return payload

    def leverage_bracket(self, symbol: str, timestamp_ms: int | None = None) -> list[dict]:
        payload = self._signed_request(
            "GET",
            "/fapi/v1/leverageBracket",
            {"symbol": symbol.upper()},
            timestamp_ms,
        )
        if not isinstance(payload, list):
            raise ValueError("leverage bracket response must be a list")
        return payload

    def open_orders(self, symbol: str | None = None, timestamp_ms: int | None = None) -> list[dict]:
        params = {"symbol": symbol.upper()} if symbol else {}
        payload = self._signed_request("GET", "/fapi/v1/openOrders", params, timestamp_ms)
        if not isinstance(payload, list):
            raise ValueError("open orders response must be a list")
        return payload

    def all_algo_orders(self, symbol: str | None = None, timestamp_ms: int | None = None) -> list[dict]:
        params = {"symbol": symbol.upper()} if symbol else {}
        payload = self._signed_request("GET", "/fapi/v1/allAlgoOrders", params, timestamp_ms)
        if not isinstance(payload, list):
            raise ValueError("algo orders response must be a list")
        return payload

    def income(self, start_time_ms: int, end_time_ms: int, timestamp_ms: int | None = None) -> list[dict]:
        payload = self._signed_request(
            "GET",
            "/fapi/v1/income",
            {"startTime": str(start_time_ms), "endTime": str(end_time_ms), "limit": "1000"},
            timestamp_ms,
        )
        if not isinstance(payload, list):
            raise ValueError("income response must be a list")
        return payload

    def change_margin_type(self, symbol: str, margin_type: str = "ISOLATED", timestamp_ms: int | None = None) -> dict:
        return self._signed_request(
            "POST",
            "/fapi/v1/marginType",
            {"symbol": symbol.upper(), "marginType": margin_type.upper()},
            timestamp_ms,
        )

    def change_leverage(self, symbol: str, leverage: int, timestamp_ms: int | None = None) -> dict:
        return self._signed_request(
            "POST",
            "/fapi/v1/leverage",
            {"symbol": symbol.upper(), "leverage": str(leverage)},
            timestamp_ms,
        )

    def create_market_order(
        self,
        symbol: str,
        side: FuturesOrderSide,
        quantity: Decimal,
        client_order_id: str,
        *,
        reduce_only: bool,
        timestamp_ms: int | None = None,
    ) -> dict:
        params = {
            "symbol": symbol.upper(),
            "side": side.value,
            "type": "MARKET",
            "quantity": format(quantity, "f"),
            "newClientOrderId": client_order_id,
        }
        if reduce_only:
            params["reduceOnly"] = "true"
        return self._signed_request("POST", "/fapi/v1/order", params, timestamp_ms)

    def query_order(self, symbol: str, client_order_id: str, timestamp_ms: int | None = None) -> dict:
        return self._signed_request(
            "GET",
            "/fapi/v1/order",
            {"symbol": symbol.upper(), "origClientOrderId": client_order_id},
            timestamp_ms,
        )

    def create_conditional_algo_order(
        self,
        symbol: str,
        side: FuturesOrderSide,
        order_type: str,
        trigger_price: Decimal,
        quantity: Decimal,
        client_algo_id: str,
        timestamp_ms: int | None = None,
    ) -> dict:
        return self._signed_request(
            "POST",
            "/fapi/v1/algoOrder",
            {
                "symbol": symbol.upper(),
                "side": side.value,
                "algoType": "CONDITIONAL",
                "type": order_type,
                "triggerPrice": format(trigger_price, "f"),
                "quantity": format(quantity, "f"),
                "reduceOnly": "true",
                "workingType": "MARK_PRICE",
                "clientAlgoId": client_algo_id,
            },
            timestamp_ms,
        )

    def cancel_algo_order(self, client_algo_id: str, timestamp_ms: int | None = None) -> dict:
        return self._signed_request(
            "DELETE",
            "/fapi/v1/algoOrder",
            {"clientAlgoId": client_algo_id},
            timestamp_ms,
        )

    def _signed_request(
        self,
        method: str,
        path: str,
        params: dict[str, str],
        timestamp_ms: int | None = None,
    ) -> Any:
        headers = {"X-MBX-APIKEY": self.api_key}
        request_base_urls = self._request_base_urls(method)
        for index, base_url in enumerate(request_base_urls):
            signed_params = self._signed_params(params, timestamp_ms=timestamp_ms, base_url=base_url)
            request_kwargs = {"params": signed_params} if method.upper() == "GET" else {"data": signed_params}
            try:
                response = self._client.request(method, f"{base_url}{path}", headers=headers, **request_kwargs)
                response.raise_for_status()
                payload = response.json()
            except httpx.TransportError as exc:
                self._audit_api_call(method, path, signed_params, headers=headers, signed=True, error=exc)
                if self._should_retry_transport_error(method, index, request_base_urls):
                    continue
                raise
            except httpx.HTTPStatusError as exc:
                self._audit_api_call(method, path, signed_params, headers=headers, signed=True, error=exc)
                if self._should_retry_http_status_error(method, index, request_base_urls, exc):
                    continue
                raise
            except Exception as exc:
                self._audit_api_call(method, path, signed_params, headers=headers, signed=True, error=exc)
                raise
            self.base_url = base_url
            self._audit_api_call(
                method,
                path,
                signed_params,
                headers=headers,
                signed=True,
                status_code=response.status_code,
                payload=payload,
            )
            return payload
        raise RuntimeError("unreachable signed futures request retry state")

    def _signed_params(self, params: dict[str, str], *, timestamp_ms: int | None, base_url: str) -> dict[str, str]:
        signed_params = dict(params)
        signed_params["recvWindow"] = str(self.recv_window)
        signed_params["timestamp"] = str(timestamp_ms if timestamp_ms is not None else self._timestamp_ms(base_url))
        query = urlencode(signed_params)
        signed_params["signature"] = sign_query(query, self.api_secret)
        return signed_params

    def _public_request(self, method: str, path: str, params: dict[str, str]) -> Any:
        request_base_urls = self._request_base_urls(method)
        for index, base_url in enumerate(request_base_urls):
            try:
                response = self._client.request(method, f"{base_url}{path}", params=params)
                response.raise_for_status()
                payload = response.json()
            except httpx.TransportError as exc:
                self._audit_api_call(method, path, params, signed=False, error=exc)
                if self._should_retry_transport_error(method, index, request_base_urls):
                    continue
                raise
            except httpx.HTTPStatusError as exc:
                self._audit_api_call(method, path, params, signed=False, error=exc)
                if self._should_retry_http_status_error(method, index, request_base_urls, exc):
                    continue
                raise
            except Exception as exc:
                self._audit_api_call(method, path, params, signed=False, error=exc)
                raise
            self.base_url = base_url
            self._audit_api_call(method, path, params, signed=False, status_code=response.status_code, payload=payload)
            return payload
        raise RuntimeError("unreachable public futures request retry state")

    def _timestamp_ms(self, base_url: str | None = None) -> int:
        if self._server_time_offset_ms is None:
            self._server_time_offset_ms = self._fetch_server_time_offset_ms(base_url)
        return int(self._clock() * 1000) + self._server_time_offset_ms

    def _fetch_server_time_offset_ms(self, base_url: str | None = None) -> int:
        resolved_base_url = base_url or self.base_url
        try:
            response = self._client.request("GET", f"{resolved_base_url}/fapi/v1/time", params={})
            response.raise_for_status()
            payload = response.json()
            server_time = int(payload["serverTime"])
        except Exception:
            return 0
        return server_time - int(self._clock() * 1000)

    def _request_base_urls(self, method: str) -> tuple[str, ...]:
        if method.upper() != "GET":
            return (self.base_url,)
        return (self.base_url, *(base_url for base_url in self.base_urls if base_url != self.base_url))

    def _should_retry_transport_error(self, method: str, index: int, request_base_urls: tuple[str, ...]) -> bool:
        return method.upper() == "GET" and index < len(request_base_urls) - 1

    def _should_retry_http_status_error(
        self,
        method: str,
        index: int,
        request_base_urls: tuple[str, ...],
        error: httpx.HTTPStatusError,
    ) -> bool:
        status_code = error.response.status_code
        return (
            method.upper() == "GET"
            and index < len(request_base_urls) - 1
            and (status_code in RETRYABLE_HTTP_STATUS_CODES or status_code >= 500)
        )

    def _audit_api_call(
        self,
        method: str,
        path: str,
        params: dict[str, str],
        *,
        signed: bool,
        headers: dict[str, str] | None = None,
        status_code: int | None = None,
        payload: Any | None = None,
        error: Exception | None = None,
    ) -> None:
        try:
            self.audit_logger.log(
                "binance.api",
                params={
                    "method": method.upper(),
                    "path": path,
                    "signed": signed,
                    "params": params,
                    **({"headers": headers} if headers else {}),
                },
                result=(
                    None
                    if error is not None
                    else {
                        "status_code": status_code,
                        "payload": payload,
                    }
                ),
                error=_safe_audit_error(error) if error is not None else None,
                level="ERROR" if error is not None else "INFO",
            )
        except Exception:
            pass


def parse_position(row: dict) -> FuturesPosition:
    return FuturesPosition(
        symbol=str(row["symbol"]),
        quantity=Decimal(str(row.get("positionAmt", "0"))),
        entry_price=Decimal(str(row.get("entryPrice", "0"))),
        mark_price=Decimal(str(row.get("markPrice", "0"))),
        liquidation_price=Decimal(str(row.get("liquidationPrice", "0"))),
        isolated_margin=Decimal(str(row.get("isolatedMargin", row.get("isolatedWallet", "0")))),
        unrealized_pnl=Decimal(str(row.get("unRealizedProfit", "0"))),
        leverage=int(row.get("leverage", "1")),
        margin_type=str(row.get("marginType", "")),
    )


def _safe_audit_error(error: Exception) -> dict[str, Any]:
    if isinstance(error, httpx.HTTPStatusError):
        response = error.response
        return {
            "type": type(error).__name__,
            "message": f"HTTP status error: {response.status_code} {response.reason_phrase}".strip(),
            "status_code": response.status_code,
        }
    if isinstance(error, httpx.RequestError):
        return {
            "type": type(error).__name__,
            "message": "HTTP request error",
        }
    return {
        "type": type(error).__name__,
        "message": str(error),
    }
