"""
KuCoin (direct REST) client (spot).

Signing (v2):
- KC-API-SIGN = base64(hmac_sha256(secret, timestamp + method + requestPathWithQuery + body))
- KC-API-PASSPHRASE = base64(hmac_sha256(secret, passphrase))
- KC-API-KEY-VERSION: 2
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import time
from decimal import Decimal, ROUND_DOWN
from typing import Any, Dict, Optional, Tuple
from urllib.parse import urlencode

from app.services.live_trading.base import BaseRestClient, LiveOrderResult, LiveTradingError
from app.services.live_trading.symbols import to_kucoin_symbol


class KucoinSpotClient(BaseRestClient):
    def __init__(
        self,
        *,
        api_key: str,
        secret_key: str,
        passphrase: str,
        base_url: str = "https://api.kucoin.com",
        timeout_sec: float = 15.0,
    ):
        super().__init__(base_url=base_url, timeout_sec=timeout_sec)
        self.api_key = (api_key or "").strip()
        self.secret_key = (secret_key or "").strip()
        self.passphrase = (passphrase or "").strip()
        if not self.api_key or not self.secret_key or not self.passphrase:
            raise LiveTradingError("Missing KuCoin api_key/secret_key/passphrase")

    def _b64_hmac_sha256(self, key: str, msg: str) -> str:
        mac = hmac.new(key.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).digest()
        return base64.b64encode(mac).decode("utf-8")

    def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
        # passphrase must be signed (v2)
        p = self._b64_hmac_sha256(self.secret_key, self.passphrase)
        return {
            "KC-API-KEY": self.api_key,
            "KC-API-SIGN": sign,
            "KC-API-TIMESTAMP": ts_ms,
            "KC-API-PASSPHRASE": p,
            "KC-API-KEY-VERSION": "2",
            "Content-Type": "application/json",
        }

    def _signed_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None) -> Any:
        m = str(method or "GET").upper()
        ts_ms = str(int(time.time() * 1000))
        body_str = self._json_dumps(json_body) if json_body is not None else ""
        qs = ""
        if params:
            norm = {str(k): "" if v is None else str(v) for k, v in dict(params).items()}
            qs = urlencode(sorted(norm.items()), doseq=True)
        signed_path = f"{path}?{qs}" if qs else path
        prehash = f"{ts_ms}{m}{signed_path}{body_str}"
        sign = self._b64_hmac_sha256(self.secret_key, prehash)
        code, data, text = self._request(m, path, params=params, data=body_str if body_str else None, headers=self._headers(ts_ms, sign))
        if code >= 400:
            raise LiveTradingError(f"KuCoin HTTP {code}: {text[:500]}")
        return data

    def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
        code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
        if code >= 400:
            raise LiveTradingError(f"KuCoin HTTP {code}: {text[:500]}")
        return data

    def ping(self) -> bool:
        try:
            d = self._public_request("GET", "/api/v1/timestamp")
            return isinstance(d, dict) and str(d.get("code") or "") in ("200000", "0", "")
        except Exception:
            return False

    def get_accounts(self) -> Any:
        return self._signed_request("GET", "/api/v1/accounts")

    def get_ticker(self, *, symbol: str) -> Dict[str, Any]:
        raw = self._public_request("GET", "/api/v1/market/orderbook/level1", params={"symbol": to_kucoin_symbol(symbol)})
        data = raw.get("data") if isinstance(raw, dict) else None
        return data if isinstance(data, dict) else {}

    def place_limit_order(self, *, symbol: str, side: str, size: float, price: float, client_order_id: Optional[str] = None) -> LiveOrderResult:
        sd = (side or "").strip().lower()
        if sd not in ("buy", "sell"):
            raise LiveTradingError(f"Invalid side: {side}")
        qty = float(size or 0.0)
        px = float(price or 0.0)
        if qty <= 0 or px <= 0:
            raise LiveTradingError("Invalid size/price")
        body: Dict[str, Any] = {
            "clientOid": str(client_order_id or str(int(time.time() * 1000))),
            "side": sd,
            "symbol": to_kucoin_symbol(symbol),
            "type": "limit",
            "price": str(px),
            "size": str(qty),
            "timeInForce": "GTC",
        }
        raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
        oid = ""
        if isinstance(raw, dict):
            d = raw.get("data")
            if isinstance(d, dict):
                oid = str(d.get("orderId") or "")
            elif isinstance(d, str):
                oid = str(d)
        return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})

    def place_market_order(
        self,
        *,
        symbol: str,
        side: str,
        size: float,
        client_order_id: Optional[str] = None,
        quote_size: bool = False,
    ) -> LiveOrderResult:
        """
        KuCoin market order:
        - sell: use size (base quantity)
        - buy: typically use funds (quote quantity). Set quote_size=True to treat `size` as funds.
        """
        sd = (side or "").strip().lower()
        if sd not in ("buy", "sell"):
            raise LiveTradingError(f"Invalid side: {side}")
        qty = float(size or 0.0)
        if qty <= 0:
            raise LiveTradingError("Invalid size")
        body: Dict[str, Any] = {
            "clientOid": str(client_order_id or str(int(time.time() * 1000))),
            "side": sd,
            "symbol": to_kucoin_symbol(symbol),
            "type": "market",
        }
        if sd == "buy" and quote_size:
            body["funds"] = str(qty)
        else:
            body["size"] = str(qty)
        raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
        oid = ""
        if isinstance(raw, dict):
            d = raw.get("data")
            if isinstance(d, dict):
                oid = str(d.get("orderId") or "")
            elif isinstance(d, str):
                oid = str(d)
        return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})

    def cancel_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
        if order_id:
            return self._signed_request("DELETE", f"/api/v1/orders/{str(order_id)}")
        if client_order_id:
            return self._signed_request("DELETE", f"/api/v1/order/client-order/{str(client_order_id)}")
        raise LiveTradingError("KuCoin cancel_order requires order_id or client_order_id")

    def get_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
        if order_id:
            return self._signed_request("GET", f"/api/v1/orders/{str(order_id)}")
        if client_order_id:
            return self._signed_request("GET", f"/api/v1/order/client-order/{str(client_order_id)}")
        raise LiveTradingError("KuCoin get_order requires order_id or client_order_id")

    def get_fills(self, *, order_id: str) -> Any:
        return self._signed_request("GET", "/api/v1/fills", params={"orderId": str(order_id)})

    def wait_for_fill(self, *, order_id: str, max_wait_sec: float = 10.0, poll_interval_sec: float = 0.5) -> Dict[str, Any]:
        end_ts = time.time() + float(max_wait_sec or 0.0)
        last: Dict[str, Any] = {}
        while True:
            timed_out = time.time() >= end_ts
            try:
                resp = self.get_order(order_id=str(order_id))
                last = resp if isinstance(resp, dict) else {"raw": resp}
            except Exception:
                last = last or {}
            data = last.get("data") if isinstance(last, dict) else None
            od = data if isinstance(data, dict) else {}
            status = str(od.get("isActive") if od else "")
            filled = 0.0
            avg_price = 0.0
            fee = 0.0
            fee_ccy = ""
            try:
                filled = float(od.get("dealSize") or 0.0)
            except Exception:
                filled = 0.0
            try:
                funds = float(od.get("dealFunds") or 0.0)
                if filled > 0 and funds > 0:
                    avg_price = funds / filled
            except Exception:
                avg_price = 0.0
            try:
                fee = abs(float(od.get("fee") or 0.0))
            except Exception:
                fee = 0.0
            fee_ccy = str(od.get("feeCurrency") or "").strip()
            if filled > 0 and avg_price > 0:
                if fee <= 0 and not timed_out:
                    time.sleep(float(poll_interval_sec or 0.5))
                    continue
                return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
            # If order is inactive, consider it terminal
            try:
                is_active = bool(od.get("isActive"))
            except Exception:
                is_active = False
            if not is_active:
                if fee <= 0 and filled > 0 and avg_price > 0 and not timed_out:
                    time.sleep(float(poll_interval_sec or 0.5))
                    continue
                return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
            if timed_out:
                return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
            time.sleep(float(poll_interval_sec or 0.5))


class KucoinFuturesClient(BaseRestClient):
    """
    KuCoin Futures (USDT perpetual) direct REST client.

    Notes:
    - Base URL typically: https://api-futures.kucoin.com
    - Auth headers/signing are the same KC-API-* style as spot (v2 passphrase signing),
      but endpoints and symbol formats differ.
    - Futures order size is typically in contracts; we convert from "base qty" best-effort.
    """

    def __init__(
        self,
        *,
        api_key: str,
        secret_key: str,
        passphrase: str,
        base_url: str = "https://api-futures.kucoin.com",
        timeout_sec: float = 15.0,
    ):
        super().__init__(base_url=base_url, timeout_sec=timeout_sec)
        self.api_key = (api_key or "").strip()
        self.secret_key = (secret_key or "").strip()
        self.passphrase = (passphrase or "").strip()
        if not self.api_key or not self.secret_key or not self.passphrase:
            raise LiveTradingError("Missing KuCoin Futures api_key/secret_key/passphrase")

        # Best-effort contract cache: symbol -> (ts, contract_dict)
        self._contract_cache: Dict[str, Tuple[float, Dict[str, Any]]] = {}
        self._contract_cache_ttl_sec = 300.0

    def _b64_hmac_sha256(self, key: str, msg: str) -> str:
        mac = hmac.new(key.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).digest()
        return base64.b64encode(mac).decode("utf-8")

    def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
        p = self._b64_hmac_sha256(self.secret_key, self.passphrase)
        return {
            "KC-API-KEY": self.api_key,
            "KC-API-SIGN": sign,
            "KC-API-TIMESTAMP": ts_ms,
            "KC-API-PASSPHRASE": p,
            "KC-API-KEY-VERSION": "2",
            "Content-Type": "application/json",
        }

    def _signed_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None) -> Any:
        m = str(method or "GET").upper()
        ts_ms = str(int(time.time() * 1000))
        body_str = self._json_dumps(json_body) if json_body is not None else ""
        qs = ""
        if params:
            norm = {str(k): "" if v is None else str(v) for k, v in dict(params).items()}
            qs = urlencode(sorted(norm.items()), doseq=True)
        signed_path = f"{path}?{qs}" if qs else path
        prehash = f"{ts_ms}{m}{signed_path}{body_str}"
        sign = self._b64_hmac_sha256(self.secret_key, prehash)
        code, data, text = self._request(m, path, params=params, data=body_str if body_str else None, headers=self._headers(ts_ms, sign))
        if code >= 400:
            raise LiveTradingError(f"KuCoinFutures HTTP {code}: {text[:500]}")
        return data

    def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
        code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
        if code >= 400:
            raise LiveTradingError(f"KuCoinFutures HTTP {code}: {text[:500]}")
        return data

    def ping(self) -> bool:
        try:
            d = self._public_request("GET", "/api/v1/timestamp")
            return isinstance(d, dict) and str(d.get("code") or "") in ("200000", "0", "")
        except Exception:
            return False

    def get_contract(self, *, symbol: str) -> Dict[str, Any]:
        from app.services.live_trading.symbols import to_kucoin_futures_symbol

        sym = to_kucoin_futures_symbol(symbol)
        if not sym:
            return {}
        now = time.time()
        cached = self._contract_cache.get(sym)
        if cached:
            ts, obj = cached
            if obj and (now - float(ts or 0.0)) <= float(self._contract_cache_ttl_sec or 300.0):
                return obj
        # KuCoin futures active contracts list
        raw = self._public_request("GET", "/api/v1/contracts/active")
        data = (raw.get("data") if isinstance(raw, dict) else None) or []
        found: Dict[str, Any] = {}
        if isinstance(data, list):
            for it in data:
                if not isinstance(it, dict):
                    continue
                if str(it.get("symbol") or "").upper() == sym.upper():
                    found = it
                    break
        if found:
            self._contract_cache[sym] = (now, found)
        return found

    def _base_to_contracts(self, *, symbol: str, base_size: float) -> int:
        """
        Convert base-asset qty -> contracts best-effort using multiplier.
        """
        from app.services.live_trading.symbols import to_kucoin_futures_symbol

        req = Decimal(str(base_size or 0.0))
        if req <= 0:
            return 0
        sym = to_kucoin_futures_symbol(symbol)
        meta: Dict[str, Any] = {}
        try:
            meta = self.get_contract(symbol=sym) or {}
        except Exception:
            meta = {}
        # multiplier is base per contract for many KuCoin perps (best-effort)
        mult = Decimal(str(meta.get("multiplier") or meta.get("lotSize") or "0"))
        if mult <= 0:
            mult = Decimal("1")
        ct = (req / mult).to_integral_value(rounding=ROUND_DOWN)
        try:
            return int(ct)
        except Exception:
            return 0

    def get_accounts(self) -> Any:
        # Futures account overview
        return self._signed_request("GET", "/api/v1/account-overview", params={"currency": "USDT"})

    def get_positions(self) -> Any:
        return self._signed_request("GET", "/api/v1/positions")

    def set_leverage(self, *, symbol: str, leverage: float) -> bool:
        from app.services.live_trading.symbols import to_kucoin_futures_symbol

        sym = to_kucoin_futures_symbol(symbol)
        try:
            lv = int(float(leverage or 1.0))
        except Exception:
            lv = 1
        if lv < 1:
            lv = 1
        body = {"symbol": sym, "leverage": str(lv)}
        try:
            _ = self._signed_request("POST", "/api/v1/position/leverage", json_body=body)
            return True
        except Exception:
            return False

    def place_market_order(
        self,
        *,
        symbol: str,
        side: str,
        size: float,
        reduce_only: bool = False,
        client_order_id: Optional[str] = None,
    ) -> LiveOrderResult:
        from app.services.live_trading.symbols import to_kucoin_futures_symbol

        sd = (side or "").strip().lower()
        if sd not in ("buy", "sell"):
            raise LiveTradingError(f"Invalid side: {side}")
        sym = to_kucoin_futures_symbol(symbol)
        qty_ct = self._base_to_contracts(symbol=sym, base_size=float(size or 0.0))
        if qty_ct <= 0:
            raise LiveTradingError("Invalid size (converted contracts <= 0)")
        body: Dict[str, Any] = {
            "clientOid": str(client_order_id or str(int(time.time() * 1000))),
            "side": sd,
            "symbol": sym,
            "type": "market",
            "size": qty_ct,
        }
        if reduce_only:
            body["reduceOnly"] = True
        raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
        oid = ""
        if isinstance(raw, dict):
            d = raw.get("data")
            if isinstance(d, dict):
                oid = str(d.get("orderId") or "")
            elif isinstance(d, str):
                oid = str(d)
        return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})

    def place_limit_order(
        self,
        *,
        symbol: str,
        side: str,
        size: float,
        price: float,
        reduce_only: bool = False,
        post_only: bool = False,
        client_order_id: Optional[str] = None,
    ) -> LiveOrderResult:
        from app.services.live_trading.symbols import to_kucoin_futures_symbol

        sd = (side or "").strip().lower()
        if sd not in ("buy", "sell"):
            raise LiveTradingError(f"Invalid side: {side}")
        sym = to_kucoin_futures_symbol(symbol)
        px = float(price or 0.0)
        if px <= 0:
            raise LiveTradingError("Invalid price")
        qty_ct = self._base_to_contracts(symbol=sym, base_size=float(size or 0.0))
        if qty_ct <= 0:
            raise LiveTradingError("Invalid size (converted contracts <= 0)")
        body: Dict[str, Any] = {
            "clientOid": str(client_order_id or str(int(time.time() * 1000))),
            "side": sd,
            "symbol": sym,
            "type": "limit",
            "price": str(px),
            "size": qty_ct,
        }
        if reduce_only:
            body["reduceOnly"] = True
        if post_only:
            body["postOnly"] = True
        raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
        oid = ""
        if isinstance(raw, dict):
            d = raw.get("data")
            if isinstance(d, dict):
                oid = str(d.get("orderId") or "")
            elif isinstance(d, str):
                oid = str(d)
        return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})

    def cancel_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
        if order_id:
            return self._signed_request("DELETE", f"/api/v1/orders/{str(order_id)}")
        if client_order_id:
            return self._signed_request("DELETE", f"/api/v1/orders/client-order/{str(client_order_id)}")
        raise LiveTradingError("KuCoinFutures cancel_order requires order_id or client_order_id")

    def get_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
        if order_id:
            return self._signed_request("GET", f"/api/v1/orders/{str(order_id)}")
        if client_order_id:
            return self._signed_request("GET", f"/api/v1/orders/byClientOid", params={"clientOid": str(client_order_id)})
        raise LiveTradingError("KuCoinFutures get_order requires order_id or client_order_id")

    def wait_for_fill(self, *, order_id: str, max_wait_sec: float = 12.0, poll_interval_sec: float = 0.5) -> Dict[str, Any]:
        end_ts = time.time() + float(max_wait_sec or 0.0)
        last: Dict[str, Any] = {}
        while True:
            timed_out = time.time() >= end_ts
            try:
                resp = self.get_order(order_id=str(order_id))
                last = resp if isinstance(resp, dict) else {"raw": resp}
            except Exception:
                last = last or {}
            od = (last.get("data") if isinstance(last, dict) else None) or {}
            status = str(od.get("status") or "")
            filled = 0.0
            avg_price = 0.0
            fee = 0.0
            fee_ccy = ""
            try:
                # dealSize is in contracts; convert back to base using multiplier best-effort.
                deal_ct = float(od.get("dealSize") or 0.0)
            except Exception:
                deal_ct = 0.0
            try:
                deal_value = float(od.get("dealValue") or 0.0)
            except Exception:
                deal_value = 0.0
            # Best-effort: infer avg price from dealValue / (deal contracts * multiplier)
            mult = 1.0
            try:
                sym = str(od.get("symbol") or "")
                meta = self.get_contract(symbol=sym) or {}
                mult = float(meta.get("multiplier") or meta.get("lotSize") or 1.0)
                if mult <= 0:
                    mult = 1.0
            except Exception:
                mult = 1.0
            filled = abs(float(deal_ct or 0.0)) * float(mult)
            if filled > 0 and deal_value > 0:
                avg_price = float(deal_value) / float(filled)
            # Extract fee from KuCoin Futures API (orderMargin contains fee info in some cases)
            try:
                fee = abs(float(od.get("fee") or od.get("orderFee") or 0.0))
            except Exception:
                fee = 0.0
            # KuCoin Futures fees are typically in USDT
            if fee > 0:
                fee_ccy = "USDT"
            if filled > 0 and avg_price > 0:
                if fee <= 0 and not timed_out:
                    time.sleep(float(poll_interval_sec or 0.5))
                    continue
                return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
            if status.lower() in ("done", "canceled", "cancelled", "filled"):
                if fee <= 0 and filled > 0 and avg_price > 0 and not timed_out:
                    time.sleep(float(poll_interval_sec or 0.5))
                    continue
                return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
            if timed_out:
                return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
            time.sleep(float(poll_interval_sec or 0.5))


