from __future__ import annotations

import asyncio
import importlib
import inspect
from dataclasses import dataclass
from typing import Any

from binance_common.configuration import (
    ConfigurationRestAPI,
    ConfigurationWebSocketAPI,
    ConfigurationWebSocketStreams,
)

from .config import EasyBinanceConfig, load_credentials
from .errors import EasyBinanceError
from .products import (
    REST_API,
    WEBSOCKET_API,
    WEBSOCKET_STREAMS,
    PRODUCTS,
    ProductSpec,
    get_product_spec,
    normalize_api_kind,
)
from .utils import first_doc_line, to_plain_data


@dataclass(frozen=True, slots=True)
class MethodInfo:
    name: str
    signature: str
    summary: str


class EasyBinance:
    """Unified facade over official Binance derivatives SDKs."""

    def __init__(self, config: EasyBinanceConfig | None = None, **overrides: Any) -> None:
        self.config = (config or EasyBinanceConfig()).with_overrides(**overrides)
        self._clients: dict[str, Any] = {}

    def list_products(self) -> tuple[ProductSpec, ...]:
        return PRODUCTS

    def list_apis(self, product: str) -> tuple[str, ...]:
        return get_product_spec(product).supported_apis

    def list_methods(self, product: str, api_kind: str) -> list[MethodInfo]:
        client = self._get_api_client(product, api_kind)
        methods: list[MethodInfo] = []
        for name in dir(client):
            if name.startswith("_"):
                continue
            attr = getattr(client, name)
            if not callable(attr):
                continue
            try:
                signature = str(inspect.signature(attr))
            except (TypeError, ValueError):
                signature = "()"
            methods.append(
                MethodInfo(
                    name=name,
                    signature=signature,
                    summary=first_doc_line(attr),
                )
            )
        return sorted(methods, key=lambda item: item.name)

    def describe_method(self, product: str, api_kind: str, method_name: str) -> dict[str, Any]:
        client = self._get_api_client(product, api_kind)
        method = getattr(client, method_name, None)
        if method is None or not callable(method):
            raise EasyBinanceError(
                f"Method '{method_name}' not found on {product} {normalize_api_kind(api_kind)}."
            )
        return {
            "product": get_product_spec(product).name,
            "api": normalize_api_kind(api_kind),
            "method": method_name,
            "signature": str(inspect.signature(method)),
            "doc": inspect.getdoc(method) or "",
        }

    def call(
        self,
        product: str,
        api_kind: str,
        method_name: str,
        params: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        normalized_api = normalize_api_kind(api_kind)
        if normalized_api == REST_API:
            client = self._get_api_client(product, normalized_api)
            method = getattr(client, method_name, None)
            if method is None or not callable(method):
                raise EasyBinanceError(f"Unknown method '{method_name}' for {product} {normalized_api}.")
            result = method(**(params or {}))
            return self._normalize_response(result)

        if normalized_api == WEBSOCKET_API:
            return asyncio.run(self._call_websocket_api(product, method_name, params or {}))

        raise EasyBinanceError(
            "Use listen() or the CLI listen command for websocket-streams methods."
        )

    def listen(
        self,
        product: str,
        method_name: str,
        params: dict[str, Any] | None = None,
        duration: float | None = 5,
        max_messages: int | None = None,
    ) -> list[Any]:
        return asyncio.run(
            self._listen_websocket_stream(
                product=product,
                method_name=method_name,
                params=params or {},
                duration=duration,
                max_messages=max_messages,
            )
        )

    async def _call_websocket_api(
        self,
        product: str,
        method_name: str,
        params: dict[str, Any],
    ) -> dict[str, Any]:
        client = self._get_api_client(product, WEBSOCKET_API)
        connection = None
        try:
            connection = await client.create_connection()
            if connection is None:
                await client.close_connection(close_session=True)
                raise EasyBinanceError(
                    f"Failed to establish websocket connection for {product} {WEBSOCKET_API}."
                )
            method = getattr(connection, method_name, None)
            if method is None or not callable(method):
                raise EasyBinanceError(
                    f"Unknown method '{method_name}' for {product} {WEBSOCKET_API}."
                )
            result = await method(**params)
            return self._normalize_response(result)
        finally:
            if connection is not None:
                await connection.close_connection(close_session=True)

    async def _listen_websocket_stream(
        self,
        product: str,
        method_name: str,
        params: dict[str, Any],
        duration: float | None,
        max_messages: int | None,
    ) -> list[Any]:
        client = self._get_api_client(product, WEBSOCKET_STREAMS)
        connection = None
        stream = None
        messages: list[Any] = []
        done = asyncio.Event()
        loop = asyncio.get_running_loop()

        def handle_message(message: Any) -> None:
            messages.append(to_plain_data(message))
            if max_messages and len(messages) >= max_messages:
                loop.call_soon_threadsafe(done.set)

        try:
            connection = await client.create_connection()
            if connection is None:
                await client.close_connection(close_session=True)
                raise EasyBinanceError(
                    f"Failed to establish websocket connection for {product} {WEBSOCKET_STREAMS}."
                )
            method = getattr(connection, method_name, None)
            if method is None or not callable(method):
                raise EasyBinanceError(
                    f"Unknown method '{method_name}' for {product} {WEBSOCKET_STREAMS}."
                )
            stream = await method(**params)
            stream.on("message", handle_message)

            if duration and duration > 0:
                try:
                    await asyncio.wait_for(done.wait(), timeout=duration)
                except asyncio.TimeoutError:
                    pass
            elif max_messages:
                await done.wait()
            else:
                while True:
                    await asyncio.sleep(0.5)
        finally:
            if stream is not None:
                await stream.unsubscribe()
            if connection is not None:
                await connection.close_connection(close_session=True)
        return messages

    def _get_api_client(self, product: str, api_kind: str) -> Any:
        normalized_api = normalize_api_kind(api_kind)
        product_client = self._get_product_client(product)
        attr_name = {
            REST_API: "rest_api",
            WEBSOCKET_API: "websocket_api",
            WEBSOCKET_STREAMS: "websocket_streams",
        }[normalized_api]
        if not hasattr(product_client, attr_name):
            raise EasyBinanceError(f"{get_product_spec(product).name} does not support {normalized_api}.")
        return getattr(product_client, attr_name)

    def _get_product_client(self, product: str) -> Any:
        spec = get_product_spec(product)
        if spec.name in self._clients:
            return self._clients[spec.name]

        credentials = load_credentials(
            env_file=self.config.env_file,
            api_key=self.config.api_key,
            api_secret=self.config.api_secret,
        )

        config_rest = ConfigurationRestAPI(
            api_key=credentials.api_key,
            api_secret=credentials.api_secret,
            base_path=self.config.rest_url or (spec.rest_testnet_url if self.config.testnet else None),
            timeout=self.config.rest_timeout,
            proxy=self.config.proxy,
            keep_alive=self.config.keep_alive,
            compression=self.config.rest_compression,
            retries=self.config.retries,
            backoff=self.config.backoff,
            time_unit=self.config.time_unit,
        )

        config_ws_api = ConfigurationWebSocketAPI(
            api_key=credentials.api_key,
            api_secret=credentials.api_secret,
            stream_url=self.config.ws_api_url or (spec.ws_api_testnet_url if self.config.testnet else None),
            timeout=self.config.ws_timeout,
            reconnect_delay=self.config.reconnect_delay,
            compression=self.config.ws_compression,
            proxy=self.config.proxy,
            pool_size=self.config.pool_size,
            time_unit=self.config.time_unit,
            return_rate_limits=self.config.return_rate_limits,
        )

        config_ws_streams = ConfigurationWebSocketStreams(
            stream_url=self.config.ws_stream_url or (spec.ws_streams_testnet_url if self.config.testnet else None),
            reconnect_delay=self.config.reconnect_delay,
            compression=self.config.ws_compression,
            proxy=self.config.proxy,
            pool_size=self.config.pool_size,
            time_unit=self.config.time_unit,
        )

        try:
            module = importlib.import_module(spec.import_path)
            client_cls = getattr(module, spec.class_name)
        except Exception as exc:
            raise EasyBinanceError(
                f"Failed to import official Binance SDK for {spec.name}: {exc}"
            ) from exc

        kwargs = {}
        init_signature = inspect.signature(client_cls)
        if "config_rest_api" in init_signature.parameters:
            kwargs["config_rest_api"] = config_rest
        if "config_ws_api" in init_signature.parameters:
            kwargs["config_ws_api"] = config_ws_api
        if "config_ws_streams" in init_signature.parameters:
            kwargs["config_ws_streams"] = config_ws_streams

        client = client_cls(**kwargs)
        self._clients[spec.name] = client
        return client

    def _normalize_response(self, response: Any) -> dict[str, Any]:
        if hasattr(response, "response") and hasattr(response, "stream"):
            return {
                "response": self._normalize_response(response.response),
                "stream": str(type(response.stream).__name__),
            }

        if hasattr(response, "data") and callable(response.data):
            payload = {
                "data": to_plain_data(response.data()),
            }
            if hasattr(response, "status"):
                payload["status"] = response.status
            if hasattr(response, "headers"):
                payload["headers"] = to_plain_data(response.headers)
            if hasattr(response, "rate_limits"):
                payload["rate_limits"] = to_plain_data(response.rate_limits)
            return payload

        return {"data": to_plain_data(response)}
