"""
CoinSwitch PRO — Spot trading client (Python).

A single-file, drop-in client with every Spot REST endpoint wrapped as a
method. Copy this file into your project, set your API key + secret on the
SpotClient instance, and call methods directly.

Requirements:
    pip install cryptography requests

Usage:
    from coinswitch_spot import SpotClient

    cs = SpotClient(
        api_key="<your hex api key>",
        secret_key="<your hex secret key>",
    )

    print(cs.server_time())
    print(cs.validate_keys())
    print(cs.list_orders(open=True, exchanges=["coinswitchx"]))

    order = cs.create_order(
        side="buy", symbol="BTC/USDT", price=60000,
        quantity=0.001, exchange="c2c1",
        client_order_id="my-uuid-here",  # idempotency key
    )
"""

from __future__ import annotations

import json
import time
import urllib.parse
import uuid
from typing import Any, Iterable, Optional

import requests
from cryptography.hazmat.primitives.asymmetric import ed25519


BASE_URL_DEFAULT = "https://coinswitch.co"


class SpotClient:
    """Spot REST client. One instance per API key pair."""

    def __init__(
        self,
        api_key: str,
        secret_key: str,
        base_url: str = BASE_URL_DEFAULT,
        request_timeout: float = 10.0,
    ) -> None:
        self.api_key = api_key
        self.secret_key = secret_key
        self.base_url = base_url.rstrip("/")
        self.timeout = request_timeout
        self._secret = ed25519.Ed25519PrivateKey.from_private_bytes(
            bytes.fromhex(secret_key)
        )
        self._session = requests.Session()

    # ------------------------------------------------------------------
    # Internals
    # ------------------------------------------------------------------

    def _sign(
        self, method: str, path: str, params: Optional[dict] = None
    ) -> tuple[dict, str]:
        method = method.upper()
        if params:
            sep = "&" if "?" in path else "?"
            path = path + sep + urllib.parse.urlencode(params)
        decoded_path = urllib.parse.unquote_plus(path)

        epoch = str(int(time.time() * 1000))
        message = method + decoded_path + epoch
        signature = self._secret.sign(message.encode("utf-8")).hex()

        headers = {
            "Content-Type": "application/json",
            "X-AUTH-APIKEY": self.api_key,
            "X-AUTH-SIGNATURE": signature,
            "X-AUTH-EPOCH": epoch,
        }
        return headers, decoded_path

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[dict] = None,
        body: Optional[dict] = None,
        signed: bool = True,
    ) -> Any:
        if signed:
            headers, decoded_path = self._sign(method, path, params)
        else:
            headers = {"Content-Type": "application/json"}
            if params:
                sep = "&" if "?" in path else "?"
                path = path + sep + urllib.parse.urlencode(params)
            decoded_path = urllib.parse.unquote_plus(path)

        url = self.base_url + decoded_path
        kwargs: dict = {"headers": headers, "timeout": self.timeout}
        if body is not None:
            kwargs["data"] = json.dumps(body)

        r = self._session.request(method.upper(), url, **kwargs)
        r.raise_for_status()
        return r.json()

    # ------------------------------------------------------------------
    # Public / health endpoints
    # ------------------------------------------------------------------

    def server_time(self) -> Any:
        """GET /trade/api/v2/time. Unauthenticated."""
        return self._request("GET", "/trade/api/v2/time", signed=False)

    def validate_keys(self) -> Any:
        """GET /trade/api/v2/validate/keys."""
        return self._request("GET", "/trade/api/v2/validate/keys")

    def ping(self) -> Any:
        """GET /trade/api/v2/ping."""
        return self._request("GET", "/trade/api/v2/ping")

    # ------------------------------------------------------------------
    # Reference data
    # ------------------------------------------------------------------

    def active_coins(self, exchange: str) -> Any:
        """List the trading symbols available on `exchange`."""
        return self._request(
            "GET", "/trade/api/v2/coins", params={"exchange": exchange}
        )

    def exchange_precision(self, exchange: str, symbol: str) -> Any:
        """Get tick size + lot size for a symbol on an exchange."""
        return self._request(
            "POST",
            "/trade/api/v2/exchangePrecision",
            body={"exchange": exchange, "symbol": symbol},
        )

    def trade_info(self, exchange: str, symbol: Optional[str] = None) -> Any:
        """Returns precision + min/max for all (or one) symbol on `exchange`."""
        params: dict = {"exchange": exchange}
        if symbol:
            params["symbol"] = symbol
        return self._request("GET", "/trade/api/v2/tradeInfo", params=params)

    def trading_fee(self, exchange: str) -> Any:
        """Fee schedule for your account on `exchange`."""
        return self._request(
            "GET", "/trade/api/v2/tradingFee", params={"exchange": exchange}
        )

    # ------------------------------------------------------------------
    # Account
    # ------------------------------------------------------------------

    def portfolio(self) -> Any:
        """GET /trade/api/v2/user/portfolio. Returns INR-denominated balances."""
        return self._request("GET", "/trade/api/v2/user/portfolio")

    def tds(self) -> Any:
        """GET /trade/api/v2/tds. Tax deducted at source so far."""
        return self._request("GET", "/trade/api/v2/tds")

    # ------------------------------------------------------------------
    # Orders
    # ------------------------------------------------------------------

    def create_order(
        self,
        *,
        side: str,
        symbol: str,
        price: float,
        quantity: float,
        exchange: str,
        type: str = "limit",
        client_order_id: Optional[str] = None,
        expiry_period: Optional[int] = None,
    ) -> Any:
        """
        Place a spot order.

        client_order_id is your idempotency key — re-send the same value on
        retry to avoid double-placement. If you don't pass one, we auto-mint
        a UUID4. Echoed back in order responses and the order-updates socket.
        """
        body: dict = {
            "side": side,
            "symbol": symbol,
            "type": type,
            "price": price,
            "quantity": quantity,
            "exchange": exchange,
        }
        body["client_order_id"] = client_order_id or str(uuid.uuid4())
        if expiry_period is not None:
            body["expiry_period"] = expiry_period
        return self._request("POST", "/trade/api/v2/order", body=body)

    def cancel_order(self, order_id: str) -> Any:
        """DELETE /trade/api/v2/order. Cancel an open order by id."""
        return self._request(
            "DELETE", "/trade/api/v2/order", body={"order_id": order_id}
        )

    def get_order(self, order_id: str) -> Any:
        """GET /trade/api/v2/order. Lookup a single order by id."""
        return self._request(
            "GET", "/trade/api/v2/order", params={"order_id": order_id}
        )

    def list_orders(
        self,
        *,
        open: bool = True,
        count: Optional[int] = None,
        from_time: Optional[int] = None,
        to_time: Optional[int] = None,
        side: Optional[str] = None,
        symbols: Optional[Iterable[str]] = None,
        exchanges: Optional[Iterable[str]] = None,
        type: Optional[str] = None,
        status: Optional[str] = None,
    ) -> Any:
        """GET /trade/api/v2/orders. Open or closed orders with filters."""
        params: dict = {"open": str(open).lower()}
        if count is not None:
            params["count"] = count
        if from_time is not None:
            params["from_time"] = from_time
        if to_time is not None:
            params["to_time"] = to_time
        if side:
            params["side"] = side
        if symbols:
            params["symbols"] = ",".join(symbols)
        if exchanges:
            params["exchanges"] = ",".join(exchanges)
        if type:
            params["type"] = type
        if status:
            params["status"] = status
        return self._request("GET", "/trade/api/v2/orders", params=params)

    # ------------------------------------------------------------------
    # Market data
    # ------------------------------------------------------------------

    def depth(self, exchange: str, symbol: str) -> Any:
        return self._request(
            "GET",
            "/trade/api/v2/depth",
            params={"exchange": exchange, "symbol": symbol},
        )

    def candles(
        self,
        *,
        exchange: str,
        symbol: str,
        interval: int,
        start_time: int,
        end_time: int,
    ) -> Any:
        return self._request(
            "GET",
            "/trade/api/v2/candles",
            params={
                "exchange": exchange,
                "symbol": symbol,
                "interval": interval,
                "start_time": start_time,
                "end_time": end_time,
            },
        )

    def trades(self, exchange: str, symbol: str) -> Any:
        return self._request(
            "GET",
            "/trade/api/v2/trades",
            params={"exchange": exchange, "symbol": symbol},
        )

    def ticker_all_pairs(self, exchange: str) -> Any:
        return self._request(
            "GET",
            "/trade/api/v2/24hr/all-pairs/ticker",
            params={"exchange": exchange},
        )

    def ticker(self, *, symbol: str, exchange: str) -> Any:
        return self._request(
            "GET",
            "/trade/api/v2/24hr/ticker",
            params={"symbol": symbol, "exchange": exchange},
        )


# ----------------------------------------------------------------------
# Retry-safe placement helper (uses the idempotency pattern)
# ----------------------------------------------------------------------

def place_with_retry(client: SpotClient, **order_kwargs) -> Any:
    """
    Place a spot order and survive transient failures without
    double-placing. Always sends a client_order_id; on timeout / 5xx,
    queries by client_order_id before retrying.
    """
    if "client_order_id" not in order_kwargs:
        order_kwargs["client_order_id"] = str(uuid.uuid4())
    cid = order_kwargs["client_order_id"]

    last_exc = None
    for attempt in range(3):
        try:
            return client.create_order(**order_kwargs)
        except (requests.Timeout, requests.HTTPError) as e:
            last_exc = e
            # Did the original request actually land?
            try:
                lookup = client.list_orders(open=True, count=50)
                for o in lookup.get("data", {}).get("orders", []):
                    if o.get("client_order_id") == cid:
                        return {"data": o, "_recovered": True}
            except Exception:
                pass
            time.sleep(0.5 * (attempt + 1))
    raise RuntimeError(f"create_order failed after retries: {last_exc}")


if __name__ == "__main__":
    # Smoke test: prints server time. Replace api/secret to test signed calls.
    cs = SpotClient(api_key="<your api key>", secret_key="<your secret key>")
    print(cs.server_time())
