"""Hyperliquid SDK client wrapper."""

from typing import Any

from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils.constants import MAINNET_API_URL, TESTNET_API_URL
from hyperliquid.utils.signing import OrderType, Tif
from hyperliquid.utils.types import Cloid

from .config import Config, Network
from .constants.assets import (
    get_asset_info as get_cached_asset_info,
)
from .constants.assets import (
    get_coin_name as get_cached_coin_name,
)
from .constants.assets import (
    get_max_leverage as get_cached_max_leverage,
)
from .types import (
    AllMids,
    DisplayValue,
    FundingHistory,
    OpenOrders,
    UserFills,
)


class HyperliquidClient:
    """Wrapper around Hyperliquid SDK for consistent interface."""

    def __init__(self, config: Config):
        """
        Initialize the Hyperliquid client.

        Args:
            config: Application configuration
        """
        self.config = config
        self._setup_api_url()
        self.info = Info(self.api_url, True)
        self.exchange: Exchange | None = None

        if config.secret_key:
            from eth_account import Account

            wallet = Account.from_key(config.secret_key)
            self.exchange = Exchange(
                wallet=wallet,
                base_url=self.api_url,
            )

    def _setup_api_url(self) -> None:
        """Set up the API URL based on configuration."""
        if self.config.api_url:
            self.api_url = self.config.api_url
        elif self.config.network == Network.TESTNET:
            self.api_url = TESTNET_API_URL
        else:
            self.api_url = MAINNET_API_URL

    def get_all_mids(self) -> AllMids:
        """
        Get current mid prices for all coins.

        Returns:
            Dict mapping coin symbols to mid prices
        """
        response = self.info.all_mids()
        return {k: float(v) for k, v in response.items() if v is not None}

    def get_user_state(self, address: str) -> dict[str, Any]:
        """
        Get complete user state including positions and balances.

        Args:
            address: Wallet address to query

        Returns:
            User state dictionary
        """
        self._validate_address(address)
        try:
            result = self.info.user_state(address)
            return result if result is not None else {}
        except Exception as e:
            self._handle_api_error(e, f"fetch user state for {address[:6]}...{address[-4:]}")
            return {}

    def get_open_orders(self, address: str) -> OpenOrders:
        """
        Get open orders for a wallet address.

        Args:
            address: Wallet address to query

        Returns:
            List of open orders
        """
        result = self.info.open_orders(address)
        return result if result is not None else []

    def get_user_fills(self, address: str, start_time: int | None = None) -> UserFills:
        """
        Get user's trade fills.

        Args:
            address: Wallet address to query

        Returns:
            List of trade fills
        """
        if start_time is None:
            result = self.info.user_fills(address)
        else:
            fills_by_time_method = getattr(self.info, "user_fills_by_time", None)
            if fills_by_time_method:
                result = fills_by_time_method(address, start_time)
            else:
                result = self.info.user_fills(address)
        return result if result is not None else []

    def get_funding_history(
        self,
        coin: str,
        start_time: int,
        end_time: int | None = None,
    ) -> FundingHistory:
        """
        Get funding rate history for a coin.

        Args:
            coin: Coin symbol (required)
            start_time: Start timestamp in milliseconds (required)
            end_time: Optional end timestamp in milliseconds

        Returns:
            List of funding rate entries
        """
        if not coin:
            raise ValueError("Coin symbol is required for funding history")
        result = self.info.funding_history(coin, start_time, end_time)
        return result if result is not None else []

    def get_l2_book(self, coin: str) -> dict[str, Any]:
        """
        Get L2 orderbook snapshot.

        Args:
            coin: Coin symbol

        Returns:
            Orderbook snapshot with bids and asks
        """
        result = self.info.l2_snapshot(coin)
        return result if result is not None else {}

    def place_order(
        self,
        coin: str,
        is_buy: bool,
        size: float,
        limit_px: float,
        reduce_only: bool = False,
        tif: Tif = "Gtc",
        cloid: Cloid | None = None,
    ) -> dict[str, Any]:
        """
        Place a limit order.

        Args:
            coin: Coin symbol
            is_buy: True for buy, False for sell
            size: Order size
            limit_px: Limit price
            reduce_only: Whether order is reduce-only
            tif: Time in force (Gtc, Ioc, Alo)
            cloid: Client order ID

        Returns:
            Order response

        Raises:
            ValueError: If exchange client is not initialized
        """
        if not self.exchange:
            raise ValueError("Trading requires secret key configuration")

        order_type: OrderType = {"limit": {"tif": tif}}
        result = self.exchange.order(coin, is_buy, size, limit_px, order_type, reduce_only, cloid)
        return result if result is not None else {}

    def cancel_order(self, coin: str, order_id: int) -> dict[str, Any]:
        """
        Cancel an open order.

        Args:
            coin: Coin symbol
            order_id: Order ID to cancel

        Returns:
            Cancellation response

        Raises:
            ValueError: If exchange client is not initialized
        """
        if not self.exchange:
            raise ValueError("Trading requires secret key configuration")

        result = self.exchange.cancel(coin, order_id)
        return result if result is not None else {}

    def cancel_all_orders(self, coin: str | None = None) -> dict[str, Any]:
        """
        Cancel all open orders.

        Args:
            coin: Optional coin symbol to cancel orders for

        Returns:
            Cancellation response

        Raises:
            ValueError: If exchange client is not initialized
        """
        if not self.exchange:
            raise ValueError("Trading requires secret key configuration")

        if not self.config.account_address:
            raise ValueError("Account address is required for canceling orders")

        open_orders = self.get_open_orders(self.config.account_address)
        results = []

        for order in open_orders:
            order_coin = order.get("coin", "")
            if coin is None or order_coin == coin:
                oid = order.get("oid")
                if oid:
                    result = self.exchange.cancel(order_coin, oid)
                    results.append(result)

        return {"status": "ok", "results": results}

    def get_meta(self) -> Any:
        """
        Get metadata about available assets.

        Returns:
            Metadata about available assets
        """
        result = self.info.meta()
        return result if result is not None else {}

    def get_meta_and_asset_ctxs(self) -> list[Any]:
        """
        Get metadata and asset contexts including mark prices.

        Returns:
            Raw result from SDK: [meta, asset_ctxs]
        """
        result = self.info.meta_and_asset_ctxs()
        return result if result is not None else [{}, []]

    def format_price(self, price: DisplayValue, decimals: int = 4) -> str:
        """
        Format price for display.

        Args:
            price: Price value
            decimals: Number of decimal places

        Returns:
            Formatted price string
        """
        if price is None:
            return "N/A"
        try:
            return f"{float(price):.{decimals}f}"
        except (ValueError, TypeError):
            return str(price)

    def format_size(self, size: DisplayValue, decimals: int = 6) -> str:
        """
        Format size for display.

        Args:
            size: Size value
            decimals: Number of decimal places

        Returns:
            Formatted size string
        """
        if size is None:
            return "N/A"
        try:
            return f"{float(size):.{decimals}f}"
        except (ValueError, TypeError):
            return str(size)

    def get_account_info(self, address: str) -> list[dict[str, Any]]:
        """Get comprehensive account information."""
        user_state = self.get_user_state(address)
        if not user_state:
            return []

        account_info = []
        margin_summary = user_state.get("marginSummary", {})

        account_info.append(
            {"Field": "Account Value", "Value": f"${float(margin_summary.get('accountValue', 0)):,.2f}"}
        )
        account_info.append(
            {"Field": "Total Position Value", "Value": f"${float(margin_summary.get('totalNtlPos', 0)):,.2f}"}
        )
        account_info.append(
            {"Field": "Total Margin Used", "Value": f"${float(margin_summary.get('totalMarginUsed', 0)):,.2f}"}
        )
        account_info.append({"Field": "Withdrawable", "Value": f"${float(user_state.get('withdrawable', 0)):,.2f}"})

        return account_info

    def update_leverage(self, coin: str, leverage: int, cross: bool = True) -> dict[str, Any]:
        """Update leverage for a coin position."""
        if not self.exchange:
            raise ValueError("Trading requires secret key configuration")

        result = self.exchange.update_leverage(leverage, coin, cross)
        return result if result is not None else {}

    def get_asset_info(self, coin: str) -> dict[str, Any]:
        """Get asset information from cache with API fallback."""
        is_testnet = self.config.network == Network.TESTNET
        cached_info = get_cached_asset_info(coin, is_testnet)
        if cached_info:
            return cached_info

        meta = self.get_meta()
        if "universe" not in meta:
            return {}

        if coin.startswith("@"):
            try:
                idx = int(coin[1:])
                if idx < len(meta["universe"]):
                    return dict(meta["universe"][idx])
            except ValueError:
                pass
        else:
            coin = coin.upper()
            for asset in meta["universe"]:
                if asset.get("name", "").upper() == coin:
                    return dict(asset)

        return {}

    def validate_leverage(self, coin: str, leverage: int) -> tuple[bool, str]:
        """Validate leverage using cached data with API fallback."""
        is_testnet = self.config.network == Network.TESTNET
        max_leverage = get_cached_max_leverage(coin, is_testnet)

        if max_leverage is None:
            asset_info = self.get_asset_info(coin)
            if not asset_info:
                return False, f"Asset {coin} not found"
            max_leverage = asset_info.get("maxLeverage", 1)

        if leverage > max_leverage:
            return False, f"Leverage {leverage}x exceeds maximum {max_leverage}x for {coin}"

        return True, ""

    def get_coin_name(self, coin_id: str) -> str:
        """Convert coin ID using cached data with API fallback."""
        is_testnet = self.config.network == Network.TESTNET
        cached_name = get_cached_coin_name(coin_id, is_testnet)

        if cached_name != coin_id:
            return cached_name

        if not coin_id.startswith("@"):
            return coin_id

        try:
            idx = int(coin_id[1:])
            meta = self.get_meta()
            if "universe" in meta and idx < len(meta["universe"]):
                return str(meta["universe"][idx]["name"])
        except (ValueError, KeyError, IndexError):
            pass

        return coin_id

    def _validate_address(self, address: str) -> None:
        """Validate wallet address format."""
        if not address:
            raise ValueError("Address cannot be empty")

        if not address.startswith("0x"):
            raise ValueError("Address must start with '0x'")

        if len(address) != 42:
            raise ValueError(f"Address must be 42 characters long, got {len(address)}")

        try:
            int(address[2:], 16)
        except ValueError as e:
            raise ValueError("Address must be valid hexadecimal") from e

    def calculate_position_size(self, coin: str, margin_usd: float, leverage: int) -> float:
        """Calculate position size from margin and leverage."""
        current_price = self.get_all_mids().get(coin)
        if not current_price:
            raise ValueError(f"Could not get price for {coin}")

        notional_value = margin_usd * leverage
        position_size = notional_value / current_price
        return position_size

    def get_position_size(self, coin: str, address: str) -> float:
        """Get current position size for a coin."""
        user_state = self.get_user_state(address)
        if not user_state or "assetPositions" not in user_state:
            return 0.0

        for pos in user_state["assetPositions"]:
            position = pos.get("position", {})
            if position.get("coin", "").upper() == coin.upper():
                return float(position.get("szi", 0))

        return 0.0

    def open_position(
        self,
        coin: str,
        is_long: bool,
        leverage: int,
        margin_usd: float,
        slippage: float = 1.0,
    ) -> dict[str, Any]:
        """Open a leveraged position with specified margin."""
        if not self.exchange:
            raise ValueError("Trading requires secret key configuration")

        position_size = self.calculate_position_size(coin, margin_usd, leverage)

        asset_info = self.get_asset_info(coin)
        if not asset_info:
            raise ValueError(f"Asset {coin} not found")

        sz_decimals = asset_info.get("szDecimals")
        if sz_decimals is None:
            raise ValueError(f"Size decimals not found for {coin}")
        position_size = round(position_size, sz_decimals)

        min_size = 10 ** (-sz_decimals)
        if position_size < min_size:
            raise ValueError(f"Position size {position_size} is too small (min: {min_size} {coin})")

        self.update_leverage(coin, leverage)

        result = self.exchange.market_open(
            coin,
            is_buy=is_long,
            sz=position_size,
            px=None,
            slippage=slippage / 100,
        )

        return result if result is not None else {}

    def close_position(
        self,
        coin: str,
        percentage: float,
        address: str,
        slippage: float = 1.0,
    ) -> dict[str, Any]:
        """Close a percentage of an existing position."""
        if not self.exchange:
            raise ValueError("Trading requires secret key configuration")

        current_size = self.get_position_size(coin, address)
        if current_size == 0:
            raise ValueError(f"No open position found for {coin}")

        if percentage >= 100:
            result = self.exchange.market_close(
                coin,
                sz=None,
                px=None,
                slippage=slippage / 100,
            )
        else:
            close_size = abs(current_size * percentage / 100)

            asset_info = self.get_asset_info(coin)
            if not asset_info:
                raise ValueError(f"Asset {coin} not found")

            sz_decimals = asset_info.get("szDecimals")
            if sz_decimals is None:
                raise ValueError(f"Size decimals not found for {coin}")
            close_size = round(close_size, sz_decimals)

            result = self.exchange.market_close(
                coin,
                sz=close_size,
                px=None,
                slippage=slippage / 100,
            )

        return result if result is not None else {}

    def close_all_positions(
        self,
        percentage: float,
        address: str,
        slippage: float = 1.0,
    ) -> list[dict[str, Any]]:
        """Close a percentage of all open positions."""
        if not self.exchange:
            raise ValueError("Trading requires secret key configuration")

        user_state = self.get_user_state(address)
        if not user_state or "assetPositions" not in user_state:
            return []

        results = []

        for pos in user_state["assetPositions"]:
            position = pos.get("position", {})
            coin = position.get("coin", "")
            current_size = float(position.get("szi", 0))

            if current_size == 0:
                continue

            try:
                if percentage >= 100:
                    result = self.exchange.market_close(
                        coin,
                        sz=None,
                        px=None,
                        slippage=slippage / 100,
                    )
                else:
                    close_size = abs(current_size * percentage / 100)

                    asset_info = self.get_asset_info(coin)
                    if not asset_info:
                        raise ValueError(f"Asset {coin} not found")

                    sz_decimals = asset_info.get("szDecimals")
                    if sz_decimals is None:
                        raise ValueError(f"Size decimals not found for {coin}")
                    close_size = round(close_size, sz_decimals)

                    result = self.exchange.market_close(
                        coin,
                        sz=close_size,
                        px=None,
                        slippage=slippage / 100,
                    )

                results.append({"coin": coin, "result": result})
            except Exception as e:
                results.append({"coin": coin, "error": str(e)})

        return results

    def _handle_api_error(self, error: Exception, operation: str) -> None:
        """Convert API errors to user-friendly messages."""
        error_str = str(error)
        if "422" in error_str:
            if "Failed to deserialize" in error_str:
                raise ValueError("Invalid address format or API request")
        elif "404" in error_str:
            raise ValueError("Address not found or no data available")
        elif "500" in error_str:
            raise ValueError("Hyperliquid API error - please try again")
        elif "timeout" in error_str.lower() or "connection" in error_str.lower():
            raise ValueError("Connection timeout - check your internet connection")
        else:
            raise ValueError(f"Failed to {operation}: API error")
