"""OKX V5 私有交易 REST（下单）。"""

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import logging
import time
from dataclasses import dataclass
from typing import Any

import httpx

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

logger = logging.getLogger(__name__)


@dataclass(slots=True)
class LiveFill:
    inst_id: str
    side: str
    avg_px: float
    sz: float  # 张或币，取决于合约
    fee: float
    ord_id: str
    raw: dict[str, Any]


class OkxTradeClient:
    def __init__(self, settings: Settings | None = None) -> None:
        self.settings = settings or get_settings()
        proxy = (self.settings.okx_http_proxy or "").strip() or None
        self._client = httpx.Client(
            base_url=self.settings.okx_rest_base.rstrip("/"),
            timeout=20.0,
            proxy=proxy,
            headers={"Accept": "application/json", "User-Agent": "eth-hedge-live/0.1"},
        )
        self._ct_val_cache: dict[str, float] = {}
        self._throttle = get_throttle("okx_trade", min_interval_sec=1.0)

    def close(self) -> None:
        self._client.close()

    def _ts(self) -> str:
        # OKX: ISO8601 with milliseconds
        return (
            time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
            + f".{int(time.time() * 1000) % 1000:03d}Z"
        )

    def _sign(self, ts: str, method: str, path: str, body: str) -> str:
        secret = (self.settings.okx_api_secret or "").encode("utf-8")
        msg = f"{ts}{method.upper()}{path}{body}".encode("utf-8")
        dig = hmac.new(secret, msg, hashlib.sha256).digest()
        return base64.b64encode(dig).decode("utf-8")

    def _headers(self, ts: str, sign: str) -> dict[str, str]:
        return {
            "OK-ACCESS-KEY": self.settings.okx_api_key or "",
            "OK-ACCESS-SIGN": sign,
            "OK-ACCESS-TIMESTAMP": ts,
            "OK-ACCESS-PASSPHRASE": self.settings.okx_api_passphrase or "",
            "Content-Type": "application/json",
        }

    def _request(
        self, method: str, path: str, body: dict[str, Any] | None = None
    ) -> list[dict[str, Any]]:
        self._throttle.before_request()
        payload = "" if body is None else json.dumps(body, separators=(",", ":"))
        ts = self._ts()
        sign = self._sign(ts, method, path, payload)
        headers = self._headers(ts, sign)
        if method.upper() == "GET":
            r = self._client.get(path, headers=headers)
        else:
            r = self._client.request(method.upper(), path, content=payload, headers=headers)
        if r.status_code in (418, 429):
            ra = parse_retry_after_header(r.headers)
            self._throttle.mark_http(r.status_code, ra)
            raise RateLimitError(
                f"OKX HTTP {r.status_code}: {r.text[:200]}",
                retry_after=self._throttle.remaining_cooldown(),
            )
        try:
            r.raise_for_status()
        except httpx.HTTPStatusError as e:
            raise RuntimeError(f"OKX HTTP {r.status_code}: {r.text[:300]}") from e
        data = r.json()
        code = str(data.get("code") or "")
        msg = str(data.get("msg") or "")
        # OKX 业务层频率类错误
        if code != "0":
            low = f"{code} {msg}".lower()
            if code in ("50011", "50061") or "too many" in low or "频率" in msg:
                self._throttle.mark_seconds(20.0)
                raise RateLimitError(
                    f"OKX trade rate-limited code={code} msg={msg}",
                    retry_after=self._throttle.remaining_cooldown(),
                )
            raise RuntimeError(
                f"OKX trade error code={code} msg={msg} data={data.get('data')}"
            )
        rows = data.get("data") or []
        return [x for x in rows if isinstance(x, dict)]

    def get_ct_val(self, inst_id: str, *, inst_type: str) -> float:
        if inst_id in self._ct_val_cache:
            return self._ct_val_cache[inst_id]
        r = self._client.get(
            "/api/v5/public/instruments",
            params={"instType": inst_type, "instId": inst_id},
        )
        r.raise_for_status()
        body = r.json()
        rows = body.get("data") or []
        for row in rows:
            if str(row.get("instId")) == inst_id:
                v = safe_float(row.get("ctVal")) or safe_float(row.get("ctMult"))
                if v and v > 0:
                    self._ct_val_cache[inst_id] = float(v)
                    return float(v)
        raise RuntimeError(f"OKX 无法取得合约面值 ctVal: {inst_id} instType={inst_type}")

    def place_market(
        self,
        *,
        inst_id: str,
        side: str,  # buy|sell
        sz: str,
        td_mode: str,
        pos_side: str | None = None,
        reduce_only: bool = False,
    ) -> LiveFill:
        body: dict[str, Any] = {
            "instId": inst_id,
            "tdMode": td_mode,
            "side": side,
            "ordType": "market",
            "sz": str(sz),
        }
        if pos_side:
            body["posSide"] = pos_side
        if reduce_only:
            body["reduceOnly"] = True
        rows = self._request("POST", "/api/v5/trade/order", body)
        if not rows:
            raise RuntimeError("OKX 下单无返回")
        ord_id = str(rows[0].get("ordId") or "")
        # 查单取均价
        fill = self._wait_fill(inst_id, ord_id)
        return fill

    def _fill_from_order_row(self, inst_id: str, ord_id: str, row: dict[str, Any]) -> LiveFill:
        avg = safe_float(row.get("avgPx")) or 0.0
        sz = safe_float(row.get("accFillSz")) or safe_float(row.get("sz")) or 0.0
        fee = abs(safe_float(row.get("fee")) or 0.0)
        fee_ccy = str(row.get("feeCcy") or "USDT")
        if fee <= 0 and ord_id:
            fee, fee_ccy = self.sum_fill_fees(inst_id, ord_id)
        from .money import abs_fee_usdt

        return LiveFill(
            inst_id=inst_id,
            side=str(row.get("side") or ""),
            avg_px=float(avg),
            sz=float(sz),
            fee=abs_fee_usdt(fee, fee_ccy),
            ord_id=ord_id,
            raw=row,
        )

    def _wait_fill(self, inst_id: str, ord_id: str, *, tries: int = 40) -> LiveFill:
        path = f"/api/v5/trade/order?instId={inst_id}&ordId={ord_id}"
        last: dict[str, Any] = {}
        for _ in range(tries):
            rows = self._request("GET", path)
            if rows:
                last = rows[0]
                state = str(last.get("state") or "")
                avg = safe_float(last.get("avgPx"))
                # 仅完全成交；部分成交继续等，避免账本张数与交易所不一致
                if state == "filled" and avg and avg > 0:
                    return self._fill_from_order_row(inst_id, ord_id, last)
                if state in ("canceled", "failed"):
                    raise RuntimeError(f"OKX 订单失败 state={state} {last}")
            time.sleep(0.3)
        # 超时兜底：仅接受完全成交；部分成交不得当全成记账（会错张数/对冲）
        state = str(last.get("state") or "")
        avg = safe_float(last.get("avgPx"))
        if state == "filled" and avg and avg > 0:
            logger.warning(
                "OKX fill wait timeout but order filled ordId=%s",
                ord_id,
            )
            return self._fill_from_order_row(inst_id, ord_id, last)
        raise RuntimeError(f"OKX 订单未完全成交 ordId={ord_id} last={last}")

    def sum_fill_fees(self, inst_id: str, ord_id: str) -> tuple[float, str]:
        """成交明细手续费合计（原币种金额, 币种）。"""
        path = f"/api/v5/trade/fills?instId={inst_id}&ordId={ord_id}"
        try:
            rows = self._request("GET", path)
        except Exception as e:
            logger.warning("okx fills fee query failed: %s", e)
            return 0.0, "USDT"
        total = 0.0
        ccy = "USDT"
        for row in rows:
            f = abs(safe_float(row.get("fee")) or 0.0)
            total += f
            if row.get("feeCcy"):
                ccy = str(row.get("feeCcy"))
        return total, ccy

    def get_perp_upl_usdt(self, inst_id: str, *, pos_side: str | None = None) -> float | None:
        """当前永续未实现盈亏（USDT，1:1）。"""
        from .money import to_usdt

        try:
            rows = self._request(
                "GET", f"/api/v5/account/positions?instId={inst_id}"
            )
        except Exception as e:
            logger.warning("okx positions failed: %s", e)
            return None
        want = (pos_side or "").strip().lower()
        for row in rows:
            ps = str(row.get("posSide") or "").lower()
            pos = safe_float(row.get("pos")) or 0.0
            if abs(pos) < 1e-12:
                continue
            if want and want not in ("net", "") and ps and ps != want and ps != "net":
                continue
            upl = safe_float(row.get("upl"))
            if upl is None:
                continue
            ccy = str(row.get("ccy") or row.get("settleCcy") or "USDT")
            return to_usdt(float(upl), ccy)
        return 0.0

    def get_perp_pos_sz(self, inst_id: str, *, pos_side: str | None = None) -> float | None:
        """当前永续绝对持仓张数。"""
        try:
            rows = self._request(
                "GET", f"/api/v5/account/positions?instId={inst_id}"
            )
        except Exception as e:
            logger.warning("okx get_perp_pos_sz failed: %s", e)
            return None
        want = (pos_side or "").strip().lower()
        for row in rows:
            ps = str(row.get("posSide") or "").lower()
            pos = safe_float(row.get("pos")) or 0.0
            if abs(pos) < 1e-12:
                continue
            if want and want not in ("net", "") and ps and ps != want and ps != "net":
                continue
            return abs(float(pos))
        return 0.0

    def get_option_pos_sz(self, inst_id: str) -> float | None:
        """期权绝对持仓张数。"""
        try:
            rows = self._request(
                "GET",
                f"/api/v5/account/positions?instType=OPTION&instId={inst_id}",
            )
        except Exception as e:
            logger.warning("okx get_option_pos_sz failed: %s", e)
            return None
        total = 0.0
        for row in rows:
            pos = safe_float(row.get("pos")) or 0.0
            total += abs(float(pos))
        return total

    def any_option_pos_abs(self) -> float | None:
        """账户任意期权绝对持仓张数合计。"""
        try:
            rows = self._request("GET", "/api/v5/account/positions?instType=OPTION")
        except Exception as e:
            logger.warning("okx any_option_pos_abs failed: %s", e)
            return None
        total = 0.0
        for row in rows:
            pos = safe_float(row.get("pos")) or 0.0
            total += abs(float(pos))
        return total

    def set_leverage(
        self,
        inst_id: str,
        leverage: float,
        *,
        mgn_mode: str = "cross",
        pos_side: str | None = None,
    ) -> None:
        body: dict[str, Any] = {
            "instId": inst_id,
            "lever": str(leverage),
            "mgnMode": mgn_mode,
        }
        if pos_side:
            body["posSide"] = pos_side
        self._request("POST", "/api/v5/account/set-leverage", body)

    def get_funding_usdt(
        self, inst_id: str, *, begin_ms: int, end_ms: int | None = None
    ) -> float:
        """资金费合计（已计入账户的 signed 金额，USDT 1:1）。付费为负。"""
        from .money import to_usdt

        end = int(end_ms or int(time.time() * 1000))
        # type=8 funding fee
        path = (
            f"/api/v5/account/bills?instType=SWAP&instId={inst_id}"
            f"&type=8&begin={int(begin_ms)}&end={end}"
        )
        total = 0.0
        try:
            rows = self._request("GET", path)
        except Exception as e:
            logger.warning("okx funding bills failed: %s", e)
            return 0.0
        for row in rows:
            # balChg / pnl 视接口；资金费常用 pnl 或 balChg
            raw = safe_float(row.get("pnl"))
            if raw is None:
                raw = safe_float(row.get("balChg"))
            if raw is None:
                continue
            ccy = str(row.get("ccy") or "USDT")
            total += to_usdt(float(raw), ccy)
        return total

    def get_closed_perp_pnl_usdt(
        self, inst_id: str, *, begin_ms: int, end_ms: int | None = None
    ) -> float | None:
        """平仓后从历史仓位取已实现盈亏（不含手续费；含部分仓位盈亏）。"""
        from .money import to_usdt

        end = int(end_ms or int(time.time() * 1000))
        begin = int(begin_ms)
        # OKX：after=更早时间戳边界，before=更晚；再本地按 uTime 过滤兜底
        path = (
            f"/api/v5/account/positions-history?instType=SWAP&instId={inst_id}"
            f"&after={begin}&before={end}"
        )
        try:
            rows = self._request("GET", path)
        except Exception as e:
            logger.warning("okx positions-history failed: %s", e)
            try:
                rows = self._request(
                    "GET",
                    f"/api/v5/account/positions-history?instType=SWAP&instId={inst_id}",
                )
            except Exception as e2:
                logger.warning("okx positions-history fallback failed: %s", e2)
                return None
        total = 0.0
        hit = False
        for row in rows:
            u_time = int(safe_float(row.get("uTime")) or safe_float(row.get("cTime")) or 0)
            if u_time and (u_time < begin - 60_000 or u_time > end + 60_000):
                continue
            rpnl = safe_float(row.get("realizedPnl"))
            if rpnl is None:
                rpnl = safe_float(row.get("pnl"))
            if rpnl is None:
                continue
            hit = True
            ccy = str(row.get("ccy") or "USDT")
            total += to_usdt(float(rpnl), ccy)
        return total if hit else None

