from __future__ import annotations

import hashlib
import hmac
import json
import time
from dataclasses import dataclass
from typing import Any
from urllib.parse import urlencode

import requests

from crypto_spot_bot.config import Settings
from crypto_spot_bot.models import Candle, Ticker


class BybitError(RuntimeError):
    pass


def _float(value: Any, default: float = 0.0) -> float:
    try:
        return float(value)
    except (TypeError, ValueError):
        return default


@dataclass(slots=True)
class Instrument:
    symbol: str
    base_coin: str
    quote_coin: str
    status: str
    tick_size: float
    qty_step: float
    min_order_qty: float
    min_notional_value: float


class BybitClient:
    def __init__(self, settings: Settings):
        self.settings = settings
        self.session = requests.Session()

    def public_get(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
        response = self.session.get(
            f"{self.settings.rest_base_url}{path}",
            params=params,
            timeout=12,
        )
        response.raise_for_status()
        return self._unwrap(response.json())

    def private_post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
        body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
        headers = self._headers(body)
        response = self.session.post(
            f"{self.settings.rest_base_url}{path}",
            data=body.encode("utf-8"),
            headers=headers,
            timeout=15,
        )
        response.raise_for_status()
        return self._unwrap(response.json())

    def private_get(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
        query_params = sorted((key, value) for key, value in params.items() if value is not None)
        query = urlencode(query_params)
        headers = self._headers(query)
        response = self.session.get(
            f"{self.settings.rest_base_url}{path}",
            params=query_params,
            headers=headers,
            timeout=15,
        )
        response.raise_for_status()
        return self._unwrap(response.json())

    def _headers(self, payload: str) -> dict[str, str]:
        timestamp = str(int(time.time() * 1000))
        recv_window = "5000"
        sign_payload = timestamp + self.settings.bybit_api_key + recv_window + payload
        signature = hmac.new(
            self.settings.bybit_api_secret.encode("utf-8"),
            sign_payload.encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()
        return {
            "X-BAPI-API-KEY": self.settings.bybit_api_key,
            "X-BAPI-TIMESTAMP": timestamp,
            "X-BAPI-RECV-WINDOW": recv_window,
            "X-BAPI-SIGN": signature,
            "Content-Type": "application/json",
        }

    @staticmethod
    def _unwrap(data: dict[str, Any]) -> dict[str, Any]:
        if int(data.get("retCode", -1)) != 0:
            raise BybitError(f"{data.get('retCode')}: {data.get('retMsg')}")
        result = data.get("result")
        if not isinstance(result, dict):
            raise BybitError("Bybit returned unexpected result payload")
        return result

    def spot_tickers(self) -> list[Ticker]:
        result = self.public_get("/v5/market/tickers", {"category": "spot"})
        tickers: list[Ticker] = []
        for row in result.get("list", []):
            symbol = str(row.get("symbol", "")).upper()
            last = _float(row.get("lastPrice"))
            if not symbol or last <= 0:
                continue
            tickers.append(
                Ticker(
                    symbol=symbol,
                    last_price=last,
                    bid=_float(row.get("bid1Price")),
                    ask=_float(row.get("ask1Price")),
                    turnover_24h=_float(row.get("turnover24h")),
                    volume_24h=_float(row.get("volume24h")),
                    change_24h=_float(row.get("price24hPcnt")) * 100,
                )
            )
        return tickers

    def instruments(self) -> dict[str, Instrument]:
        result = self.public_get("/v5/market/instruments-info", {"category": "spot"})
        instruments: dict[str, Instrument] = {}
        for row in result.get("list", []):
            lot = row.get("lotSizeFilter") or {}
            price = row.get("priceFilter") or {}
            symbol = str(row.get("symbol", "")).upper()
            if not symbol:
                continue
            instruments[symbol] = Instrument(
                symbol=symbol,
                base_coin=str(row.get("baseCoin", "")),
                quote_coin=str(row.get("quoteCoin", "")),
                status=str(row.get("status", "")),
                tick_size=_float(price.get("tickSize")),
                qty_step=_float(lot.get("qtyStep"), _float(lot.get("basePrecision"))),
                min_order_qty=_float(lot.get("minOrderQty")),
                min_notional_value=_float(lot.get("minNotionalValue"), _float(lot.get("minOrderAmt"))),
            )
        return instruments

    def popular_spot_symbols(self, limit: int) -> list[str]:
        instruments = self.instruments()
        rows = []
        for ticker in self.spot_tickers():
            info = instruments.get(ticker.symbol)
            if (
                info
                and info.quote_coin == "USDT"
                and info.status == "Trading"
                and ticker.turnover_24h > 0
                and not _looks_like_leveraged_token(info.base_coin)
                and not _looks_like_stablecoin(info.base_coin)
            ):
                rows.append((ticker.turnover_24h, ticker.symbol))
        rows.sort(reverse=True)
        return [symbol for _, symbol in rows[:limit]]

    def klines(self, symbol: str, interval: str, limit: int) -> list[Candle]:
        result = self.public_get(
            "/v5/market/kline",
            {"category": "spot", "symbol": symbol, "interval": interval, "limit": limit},
        )
        candles = []
        for row in result.get("list", []):
            if len(row) < 7:
                continue
            candles.append(
                Candle(
                    timestamp=int(row[0]),
                    open=_float(row[1]),
                    high=_float(row[2]),
                    low=_float(row[3]),
                    close=_float(row[4]),
                    volume=_float(row[5]),
                    turnover=_float(row[6]),
                )
            )
        candles.sort(key=lambda item: item.timestamp)
        return candles

    def orderbook_top(self, symbol: str) -> tuple[float, float]:
        result = self.public_get(
            "/v5/market/orderbook",
            {"category": "spot", "symbol": symbol, "limit": 1},
        )
        bids = result.get("b") or []
        asks = result.get("a") or []
        bid = _float(bids[0][0]) if bids else 0.0
        ask = _float(asks[0][0]) if asks else 0.0
        return bid, ask

    def place_spot_market_order(
        self,
        symbol: str,
        side: str,
        qty: float,
        market_unit: str,
        order_link_id: str,
    ) -> dict[str, Any]:
        payload = {
            "category": "spot",
            "symbol": symbol,
            "side": side,
            "orderType": "Market",
            "qty": f"{qty:.8f}".rstrip("0").rstrip("."),
            "timeInForce": "IOC",
            "isLeverage": 0,
            "orderFilter": "Order",
            "marketUnit": market_unit,
            "orderLinkId": order_link_id,
        }
        return self.private_post("/v5/order/create", payload)

    def wallet_balance(self, account_type: str = "UNIFIED", coin: str | None = None) -> dict[str, Any]:
        return self.private_get(
            "/v5/account/wallet-balance",
            {"accountType": account_type, "coin": coin},
        )

    def realtime_orders(self, *, category: str = "spot", open_only: int = 0, limit: int = 50) -> dict[str, Any]:
        return self.private_get(
            "/v5/order/realtime",
            {"category": category, "openOnly": open_only, "limit": max(1, min(limit, 50))},
        )


def websocket_subscribe_message(symbols: list[str], interval: str = "1") -> str:
    args: list[str] = []
    for symbol in symbols:
        args.extend([f"tickers.{symbol}", f"kline.{interval}.{symbol}", f"orderbook.1.{symbol}"])
    return json.dumps({"op": "subscribe", "args": args})


def _looks_like_leveraged_token(base_coin: str) -> bool:
    upper = base_coin.upper()
    return upper.endswith(("3L", "3S", "2L", "2S", "5L", "5S", "UP", "DOWN"))


def _looks_like_stablecoin(base_coin: str) -> bool:
    return base_coin.upper() in {
        "USDC",
        "USDT",
        "DAI",
        "TUSD",
        "FDUSD",
        "USDE",
        "USDD",
        "PYUSD",
        "USD1",
    }
