"""币安私有交易：USDT-M 永续 (fapi) + 欧洲期权 (eapi)。"""

from __future__ import annotations

import hashlib
import hmac
import logging
import time
from typing import Any
from urllib.parse import urlencode

import httpx

from ..config import Settings, get_settings
from ..exchange.okx.parse import safe_float
from .okx_trade import LiveFill
from .rate_limit import RateLimitError, get_throttle, parse_retry_after_header

logger = logging.getLogger(__name__)


class BinanceTradeClient:
    def __init__(self, settings: Settings | None = None) -> None:
        self.settings = settings or get_settings()
        proxy = (self.settings.binance_http_proxy or "").strip() or None
        headers = {
            "Accept": "application/json",
            "User-Agent": "eth-hedge-live/0.1",
            "X-MBX-APIKEY": self.settings.binance_api_key or "",
        }
        self._fapi = httpx.Client(
            base_url=self.settings.binance_fapi_base.rstrip("/"),
            timeout=20.0,
            proxy=proxy,
            headers=headers,
            trust_env=False,
        )
        self._eapi = httpx.Client(
            base_url=self.settings.binance_eapi_base.rstrip("/"),
            timeout=20.0,
            proxy=proxy,
            headers=headers,
            trust_env=False,
        )
        self._hedge: bool | None = None
        self._fapi_throttle = get_throttle("binance_fapi_trade", min_interval_sec=1.0)
        self._eapi_throttle = get_throttle(
            "binance_eapi_trade",
            min_interval_sec=1.0,
            cooldown_429_sec=20.0,
            cooldown_418_sec=120.0,
        )

    def close(self) -> None:
        self._fapi.close()
        self._eapi.close()

    def _sign(self, params: dict[str, Any]) -> str:
        qs = urlencode(params, doseq=True)
        secret = (self.settings.binance_api_secret or "").encode("utf-8")
        return hmac.new(secret, qs.encode("utf-8"), hashlib.sha256).hexdigest()

    def _throttle_for(self, client: httpx.Client):
        if client is self._eapi:
            return self._eapi_throttle
        return self._fapi_throttle

    def _signed(
        self,
        client: httpx.Client,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
    ) -> Any:
        throttle = self._throttle_for(client)
        throttle.before_request()
        p = dict(params or {})
        p["timestamp"] = int(time.time() * 1000)
        p["signature"] = self._sign(p)
        r = client.request(method.upper(), path, params=p)
        if r.status_code in (418, 429):
            ra = parse_retry_after_header(r.headers)
            throttle.mark_http(r.status_code, ra)
            raise RateLimitError(
                f"Binance {path} HTTP {r.status_code}: {r.text[:200]}",
                retry_after=throttle.remaining_cooldown(),
            )
        if r.status_code >= 400:
            raise RuntimeError(f"Binance {path} HTTP {r.status_code}: {r.text[:400]}")
        data = r.json()
        if isinstance(data, dict) and "code" in data and "orderId" not in data:
            code = data.get("code")
            try:
                code_i = int(code)
            except (TypeError, ValueError):
                code_i = None
            msg = str(data.get("msg") or "")
            # -1003 too many requests; -1015 too many orders
            if code_i in (-1003, -1015) or "too many" in msg.lower():
                throttle.mark_seconds(20.0)
                raise RateLimitError(
                    f"Binance rate-limited code={code} msg={msg}",
                    retry_after=throttle.remaining_cooldown(),
                )
            if code_i is not None and code_i != 0:
                raise RuntimeError(f"Binance error code={code} msg={msg}")
            if code_i is None:
                raise RuntimeError(f"Binance error code={code} msg={msg}")
        return data

    def is_hedge_mode(self) -> bool:
        if self._hedge is not None:
            return self._hedge
        try:
            data = self._signed(self._fapi, "GET", "/fapi/v1/positionSide/dual")
            self._hedge = bool(data.get("dualSidePosition") in (True, "true", "True"))
        except Exception as e:
            logger.warning("binance hedge mode probe failed: %s; assume one-way", e)
            self._hedge = False
        return self._hedge

    def place_perp_market(
        self,
        *,
        symbol: str,
        side: str,  # BUY|SELL
        qty_eth: float,
        position_side: str | None = None,  # LONG|SHORT|None
        reduce_only: bool = False,
    ) -> LiveFill:
        # ETHUSDT 数量单位为 ETH
        qty = f"{float(qty_eth):.3f}".rstrip("0").rstrip(".")
        if not qty or qty == "0":
            qty = "0.001"
        params: dict[str, Any] = {
            "symbol": symbol,
            "side": side.upper(),
            "type": "MARKET",
            "quantity": qty,
        }
        hedge = self.is_hedge_mode()
        if hedge:
            ps = (position_side or ("LONG" if side.upper() == "BUY" else "SHORT")).upper()
            params["positionSide"] = ps
        elif reduce_only:
            params["reduceOnly"] = "true"
        data = self._signed(self._fapi, "POST", "/fapi/v1/order", params)
        return self._fill_from_fapi(symbol, data)

    def _fill_from_fapi(self, symbol: str, data: dict[str, Any]) -> LiveFill:
        ord_id = str(data.get("orderId") or "")
        avg = safe_float(data.get("avgPrice"))
        sz = safe_float(data.get("executedQty"))
        if (not avg or avg <= 0) and ord_id:
            q = self._signed(
                self._fapi,
                "GET",
                "/fapi/v1/order",
                {"symbol": symbol, "orderId": ord_id},
            )
            avg = safe_float(q.get("avgPrice")) or avg
            sz = safe_float(q.get("executedQty")) or sz
            data = q
        if not avg or avg <= 0:
            raise RuntimeError(f"币安永续无成交均价 orderId={ord_id} last={data}")
        from .money import abs_fee_usdt

        fee = abs(safe_float(data.get("cumCommission")) or 0.0)
        fee_asset = str(data.get("commissionAsset") or "USDT")
        if fee <= 0 and ord_id:
            fee, fee_asset = self.sum_perp_trade_fees(symbol, ord_id)
        return LiveFill(
            inst_id=symbol,
            side=str(data.get("side") or "").lower(),
            avg_px=float(avg),
            sz=float(sz or 0),
            fee=abs_fee_usdt(fee, fee_asset),
            ord_id=ord_id,
            raw=data if isinstance(data, dict) else {},
        )

    def place_option_market(
        self,
        *,
        symbol: str,
        side: str,  # BUY|SELL
        quantity: float,
        reduce_only: bool = False,
    ) -> LiveFill:
        qty = str(int(round(quantity)))
        if qty == "0":
            qty = "1"
        params: dict[str, Any] = {
            "symbol": symbol,
            "side": side.upper(),
            "type": "MARKET",
            "quantity": qty,
        }
        if reduce_only:
            params["reduceOnly"] = "true"
        data = self._signed(self._eapi, "POST", "/eapi/v1/order", params)
        return self._fill_from_eapi(symbol, data)

    def _fill_from_eapi(self, symbol: str, data: dict[str, Any]) -> LiveFill:
        ord_id = str(data.get("orderId") or data.get("id") or "")
        avg = safe_float(data.get("avgPrice")) or safe_float(data.get("price"))
        sz = safe_float(data.get("executedQty")) or safe_float(data.get("quantity"))
        if (not avg or avg <= 0) and ord_id:
            # 轮询几轮
            for _ in range(8):
                time.sleep(0.2)
                q = self._signed(
                    self._eapi,
                    "GET",
                    "/eapi/v1/order",
                    {"symbol": symbol, "orderId": ord_id},
                )
                avg = safe_float(q.get("avgPrice")) or safe_float(q.get("price"))
                sz = safe_float(q.get("executedQty")) or safe_float(q.get("quantity"))
                st = str(q.get("status") or "").upper()
                data = q
                if avg and avg > 0 and st == "FILLED":
                    break
                if st in ("CANCELED", "REJECTED", "EXPIRED"):
                    raise RuntimeError(f"币安期权订单失败 status={st} {q}")
                if st == "PARTIALLY_FILLED":
                    continue
        if not avg or avg <= 0:
            raise RuntimeError(f"币安期权无成交均价 orderId={ord_id} last={data}")
        st_final = str(data.get("status") or "").upper()
        if st_final and st_final != "FILLED":
            raise RuntimeError(
                f"币安期权未完全成交 status={st_final} orderId={ord_id} last={data}"
            )
        from .money import abs_fee_usdt

        fee = abs(safe_float(data.get("fee")) or 0.0)
        fee_asset = "USDT"
        if fee <= 0 and ord_id:
            fee, fee_asset = self.sum_option_trade_fees(symbol, ord_id)
        return LiveFill(
            inst_id=symbol,
            side=str(data.get("side") or "").lower(),
            avg_px=float(avg),
            sz=float(sz or 0),
            fee=abs_fee_usdt(fee, fee_asset),
            ord_id=ord_id,
            raw=data if isinstance(data, dict) else {},
        )

    def sum_perp_trade_fees(self, symbol: str, order_id: str) -> tuple[float, str]:
        try:
            rows = self._signed(
                self._fapi,
                "GET",
                "/fapi/v1/userTrades",
                {"symbol": symbol, "orderId": order_id},
            )
        except Exception as e:
            logger.warning("binance perp userTrades fee failed: %s", e)
            return 0.0, "USDT"
        if not isinstance(rows, list):
            rows = [rows] if isinstance(rows, dict) else []
        total = 0.0
        asset = "USDT"
        for row in rows:
            total += abs(safe_float(row.get("commission")) or 0.0)
            if row.get("commissionAsset"):
                asset = str(row.get("commissionAsset"))
        return total, asset

    def sum_option_trade_fees(self, symbol: str, order_id: str) -> tuple[float, str]:
        try:
            rows = self._signed(
                self._eapi,
                "GET",
                "/eapi/v1/userTrades",
                {"symbol": symbol, "orderId": order_id},
            )
        except Exception as e:
            logger.warning("binance option userTrades fee failed: %s", e)
            return 0.0, "USDT"
        if not isinstance(rows, list):
            rows = [rows] if isinstance(rows, dict) else []
        total = 0.0
        asset = "USDT"
        for row in rows:
            total += abs(safe_float(row.get("commission")) or safe_float(row.get("fee")) or 0.0)
            if row.get("commissionAsset") or row.get("feeAsset"):
                asset = str(row.get("commissionAsset") or row.get("feeAsset"))
        return total, asset

    def get_perp_upl_usdt(self, symbol: str, *, position_side: str | None = None) -> float | None:
        from .money import to_usdt

        try:
            rows = self._signed(
                self._fapi, "GET", "/fapi/v2/positionRisk", {"symbol": symbol}
            )
        except Exception as e:
            logger.warning("binance positionRisk failed: %s", e)
            return None
        if isinstance(rows, dict):
            rows = [rows]
        want = (position_side or "").strip().upper()
        for row in rows:
            amt = safe_float(row.get("positionAmt")) or 0.0
            if abs(amt) < 1e-12:
                continue
            ps = str(row.get("positionSide") or "").upper()
            if want and ps and ps not in ("BOTH",) and ps != want:
                continue
            upl = safe_float(row.get("unRealizedProfit"))
            if upl is None:
                continue
            return to_usdt(float(upl), "USDT")
        return 0.0

    def get_perp_pos_sz(self, symbol: str, *, position_side: str | None = None) -> float | None:
        """当前永续绝对持仓（ETH）。"""
        try:
            rows = self._signed(
                self._fapi, "GET", "/fapi/v2/positionRisk", {"symbol": symbol}
            )
        except Exception as e:
            logger.warning("binance get_perp_pos_sz failed: %s", e)
            return None
        if isinstance(rows, dict):
            rows = [rows]
        want = (position_side or "").strip().upper()
        for row in rows:
            amt = safe_float(row.get("positionAmt")) or 0.0
            if abs(amt) < 1e-12:
                continue
            ps = str(row.get("positionSide") or "").upper()
            if want and ps and ps not in ("BOTH",) and ps != want:
                continue
            return abs(float(amt))
        return 0.0

    def get_option_pos_sz(self, symbol: str) -> float | None:
        """期权持仓绝对张数；查不到接口时返回 None。"""
        try:
            rows = self._signed(self._eapi, "GET", "/eapi/v1/position", {"symbol": symbol})
        except Exception as e:
            logger.warning("binance get_option_pos_sz failed: %s", e)
            return None
        if isinstance(rows, dict):
            rows = [rows]
        total = 0.0
        hit = False
        for row in rows:
            if not isinstance(row, dict):
                continue
            if str(row.get("symbol") or "") and str(row.get("symbol")) != symbol:
                continue
            qty = safe_float(row.get("quantity")) or safe_float(row.get("positionAmt")) or 0.0
            hit = True
            total += abs(float(qty))
        return total if hit else 0.0

    def any_option_pos_abs(self) -> float | None:
        """账户任意期权绝对持仓合计（ETH 期权）。"""
        try:
            rows = self._signed(self._eapi, "GET", "/eapi/v1/position", {})
        except Exception as e:
            logger.warning("binance any_option_pos_abs failed: %s", e)
            return None
        if isinstance(rows, dict):
            rows = [rows]
        total = 0.0
        for row in rows:
            if not isinstance(row, dict):
                continue
            sym = str(row.get("symbol") or "")
            if sym and not sym.upper().startswith("ETH"):
                continue
            qty = safe_float(row.get("quantity")) or safe_float(row.get("positionAmt")) or 0.0
            total += abs(float(qty))
        return total

    def set_margin_type(self, symbol: str, margin_type: str) -> None:
        """ISOLATED | CROSSED。"""
        mt = "ISOLATED" if str(margin_type).lower() == "isolated" else "CROSSED"
        try:
            self._signed(
                self._fapi,
                "POST",
                "/fapi/v1/marginType",
                {"symbol": symbol, "marginType": mt},
            )
        except Exception as e:
            # 已是目标模式时币安常报错，忽略
            msg = str(e).lower()
            if "no need to change" in msg or "-4046" in msg:
                return
            raise

    def set_leverage(self, symbol: str, leverage: int | float) -> None:
        lev = int(round(float(leverage)))
        if lev < 1:
            lev = 1
        self._signed(
            self._fapi,
            "POST",
            "/fapi/v1/leverage",
            {"symbol": symbol, "leverage": lev},
        )

    def get_funding_usdt(
        self, symbol: str, *, begin_ms: int, end_ms: int | None = None
    ) -> float:
        from .money import to_usdt

        end = int(end_ms or int(time.time() * 1000))
        try:
            rows = self._signed(
                self._fapi,
                "GET",
                "/fapi/v1/income",
                {
                    "symbol": symbol,
                    "incomeType": "FUNDING_FEE",
                    "startTime": int(begin_ms),
                    "endTime": end,
                    "limit": 1000,
                },
            )
        except Exception as e:
            logger.warning("binance funding income failed: %s", e)
            return 0.0
        if isinstance(rows, dict):
            rows = [rows]
        total = 0.0
        for row in rows:
            raw = safe_float(row.get("income"))
            if raw is None:
                continue
            asset = str(row.get("asset") or "USDT")
            total += to_usdt(float(raw), asset)
        return total

    def get_closed_perp_pnl_usdt(
        self, symbol: str, *, begin_ms: int, end_ms: int | None = None
    ) -> float | None:
        """用 REALIZED_PNL income 近似已实现（含部分平仓）；资金费另计。"""
        from .money import to_usdt

        end = int(end_ms or int(time.time() * 1000))
        try:
            rows = self._signed(
                self._fapi,
                "GET",
                "/fapi/v1/income",
                {
                    "symbol": symbol,
                    "incomeType": "REALIZED_PNL",
                    "startTime": int(begin_ms),
                    "endTime": end,
                    "limit": 1000,
                },
            )
        except Exception as e:
            logger.warning("binance realized income failed: %s", e)
            return None
        if isinstance(rows, dict):
            rows = [rows]
        if not rows:
            return None
        total = 0.0
        for row in rows:
            raw = safe_float(row.get("income"))
            if raw is None:
                continue
            asset = str(row.get("asset") or "USDT")
            total += to_usdt(float(raw), asset)
        return total

