from __future__ import annotations

import base64
import hashlib
import hmac
import json
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

from kraken_bot.models import Candle


class KrakenAPIError(RuntimeError):
    pass


class KrakenClient:
    def __init__(
        self,
        api_key: str | None = None,
        api_secret: str | None = None,
        base_url: str = "https://api.kraken.com",
        timeout_seconds: int = 20,
    ) -> None:
        self.api_key = api_key
        self.api_secret = api_secret
        self.base_url = base_url.rstrip("/")
        self.timeout_seconds = timeout_seconds

    def public(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        query = urllib.parse.urlencode(params or {})
        url = f"{self.base_url}/0/public/{method}"
        if query:
            url = f"{url}?{query}"
        return self._request("GET", url)

    def private(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        if not self.api_key or not self.api_secret:
            raise KrakenAPIError("KRAKEN_API_KEY and KRAKEN_API_SECRET are required for private endpoints")
        path = f"/0/private/{method}"
        payload = dict(params or {})
        payload["nonce"] = str(int(time.time() * 1000))
        post_data = urllib.parse.urlencode(payload)
        headers = {
            "API-Key": self.api_key,
            "API-Sign": self._sign(path, payload, post_data),
            "Content-Type": "application/x-www-form-urlencoded",
        }
        return self._request("POST", f"{self.base_url}{path}", post_data.encode(), headers)

    def get_ohlc(self, pair: str, interval: int, limit: int) -> list[Candle]:
        result = self.public("OHLC", {"pair": pair, "interval": interval})
        pair_keys = [key for key in result if key != "last"]
        if not pair_keys:
            raise KrakenAPIError(f"OHLC response did not include candle data for {pair}")
        rows = result[pair_keys[0]][-limit:]
        candles: list[Candle] = []
        for row in rows:
            candles.append(
                Candle(
                    time=int(row[0]),
                    open=float(row[1]),
                    high=float(row[2]),
                    low=float(row[3]),
                    close=float(row[4]),
                    vwap=float(row[5]),
                    volume=float(row[6]),
                    count=int(row[7]),
                )
            )
        return candles

    def add_market_order(
        self,
        pair: str,
        side: str,
        volume: float,
        validate: bool = True,
    ) -> dict[str, Any]:
        return self.private(
            "AddOrder",
            {
                "pair": pair,
                "type": side,
                "ordertype": "market",
                "volume": f"{volume:.8f}".rstrip("0").rstrip("."),
                "validate": "true" if validate else "false",
            },
        )

    def _sign(self, path: str, payload: dict[str, Any], post_data: str) -> str:
        encoded = (str(payload["nonce"]) + post_data).encode()
        message = path.encode() + hashlib.sha256(encoded).digest()
        secret = base64.b64decode(self.api_secret or "")
        digest = hmac.new(secret, message, hashlib.sha512).digest()
        return base64.b64encode(digest).decode()

    def _request(
        self,
        method: str,
        url: str,
        data: bytes | None = None,
        headers: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        request = urllib.request.Request(url, data=data, headers=headers or {}, method=method)
        try:
            with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
                payload = json.loads(response.read().decode("utf-8"))
        except urllib.error.URLError as exc:
            raise KrakenAPIError(f"Kraken request failed: {exc}") from exc
        errors = payload.get("error") or []
        if errors:
            raise KrakenAPIError("; ".join(errors))
        return payload.get("result", {})
