from __future__ import annotations

import asyncio
import os
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Protocol, cast
from uuid import uuid4

import httpx
from derive_client import HTTPClient as DeriveHTTPClient
from derive_client.data_types.enums import Environment as DeriveEnvironment
from derive_client.data_types.generated_models import Direction
from derive_client.data_types.generated_models import OrderType as DeriveOrderType
from derive_client.data_types.generated_models import TimeInForce as DeriveTimeInForce
from eth_account import Account
from eth_account.signers.local import LocalAccount
from hyperliquid.exchange import Exchange as HyperliquidExchange
from hyperliquid.info import Info as HyperliquidInfo
from hyperliquid.utils import constants as hyperliquid_constants
from lighter import AccountApi, ApiClient, Configuration, OrderApi
from lighter.signer_client import SignerClient

from uforia.config import AppConfig

from .models import (
    ExecutionAccountConfig,
    ExecutionCancelAllRequest,
    ExecutionCancelRequest,
    ExecutionIntent,
    ExecutionOrderSide,
    ExecutionOrderStatus,
    ExecutionOrderType,
    ExecutionQuoteRequest,
    ExecutionVenue,
    ExecutionVenueHealth,
)

SECONDS_PER_HOUR = 60 * 60


@dataclass
class LiveQuoteResponse:
    quote_id: str
    message: str
    payload: dict[str, Any]
    price: float | None = None
    expires_at: datetime | None = None
    route: str = "router"


@dataclass
class LiveOrderResponse:
    venue_order_id: str
    message: str
    payload: dict[str, Any]
    status: ExecutionOrderStatus = ExecutionOrderStatus.ACCEPTED
    filled_size: float = 0.0
    remaining_size: float | None = None
    avg_fill_price: float | None = None
    fill_events: list[dict[str, Any]] = field(default_factory=list)


@dataclass
class LiveAccountSnapshot:
    balances: list[dict[str, Any]] = field(default_factory=list)
    positions: list[dict[str, Any]] = field(default_factory=list)
    orders: list[dict[str, Any]] = field(default_factory=list)
    fills: list[dict[str, Any]] = field(default_factory=list)
    health: ExecutionVenueHealth | None = None


class LiveExecutionClient(Protocol):
    def market_snapshots(self, asset: str | None = None) -> list[dict[str, Any]]: ...

    def account_snapshot(self, asset: str | None = None) -> LiveAccountSnapshot: ...

    def request_quote(self, request: ExecutionQuoteRequest) -> LiveQuoteResponse: ...

    def place_order(self, intent: ExecutionIntent) -> LiveOrderResponse: ...

    def cancel(self, request: ExecutionCancelRequest) -> LiveOrderResponse: ...

    def cancel_all(self, request: ExecutionCancelAllRequest) -> LiveOrderResponse: ...


def create_live_client(
    *,
    config: AppConfig,
    account: ExecutionAccountConfig,
) -> LiveExecutionClient:
    if account.venue == ExecutionVenue.DERIVE:
        return DeriveLiveClient(config=config, account=account)
    if account.venue == ExecutionVenue.HYPERLIQUID:
        return HyperliquidLiveClient(config=config, account=account)
    if account.venue == ExecutionVenue.LIGHTER:
        return LighterLiveClient(config=config, account=account)
    if account.venue == ExecutionVenue.BEBOP:
        return BebopLiveClient(config=config, account=account)
    raise ValueError(f"Unsupported execution venue: {account.venue.value}")


class DeriveLiveClient:
    def __init__(self, *, config: AppConfig, account: ExecutionAccountConfig) -> None:
        self.config = config
        self.account = account
        session_key = _require_env(account.session_key_env, "session_key_env", account)
        wallet = account.wallet_address or _address_from_key(account.private_key_env)
        if account.subaccount_id is None:
            raise ValueError(f"Derive account {account.label} missing subaccount_id")
        env = DeriveEnvironment.PROD if account.environment == "mainnet" else DeriveEnvironment.TEST
        self.client = DeriveHTTPClient(
            wallet=wallet,
            session_key=session_key,
            subaccount_id=account.subaccount_id,
            env=env,
        )

    def market_snapshots(self, asset: str | None = None) -> list[dict[str, Any]]:
        rows: list[dict[str, Any]] = []
        products = self.client.markets.get_all()
        for product in products:
            symbol = getattr(product, "instrument_name", None) or getattr(product, "name", None)
            if not symbol:
                continue
            base_asset = str(
                getattr(product, "base_currency", None) or symbol.split("-", 1)[0]
            ).upper()
            if asset and base_asset != asset.upper():
                continue
            best_bid = _safe_float(getattr(product, "best_bid_price", None))
            best_ask = _safe_float(getattr(product, "best_ask_price", None))
            mark_price = _safe_float(getattr(product, "mark_price", None))
            rows.append(
                {
                    "venue": self.account.venue.value,
                    "asset": base_asset,
                    "symbol": symbol,
                    "instrument_type": "option" if "PERP" not in symbol else "perp",
                    "status": str(getattr(product, "status", "online")).lower(),
                    "tick_size": _safe_float(getattr(product, "tick_size", None)),
                    "step_size": _safe_float(getattr(product, "amount_step", None)),
                    "min_size": _safe_float(getattr(product, "min_amount", None)),
                    "min_notional": None,
                    "best_bid": best_bid,
                    "best_ask": best_ask,
                    "mark_price": mark_price,
                    "index_price": _safe_float(getattr(product, "index_price", None)),
                    "quality_flag": "ok",
                }
            )
        return rows

    def account_snapshot(self, asset: str | None = None) -> LiveAccountSnapshot:
        now = datetime.now(tz=timezone.utc)
        account = self.client.account.get()
        balances: list[dict[str, Any]] = []
        for collateral in getattr(account, "collaterals", []) or []:
            asset_name = str(getattr(collateral, "currency", "USD")).upper()
            balances.append(
                {
                    "ts": now,
                    "venue": self.account.venue.value,
                    "account": self.account.label,
                    "asset": asset_name,
                    "balance_type": "total",
                    "total": _safe_float(getattr(collateral, "balance", None)),
                    "available": _safe_float(getattr(collateral, "available_balance", None)),
                    "locked": _safe_float(getattr(collateral, "used_balance", None)),
                    "quality_flag": "ok",
                }
            )
        positions: list[dict[str, Any]] = []
        for position in self.client.active_subaccount.positions.list():
            symbol = str(getattr(position, "instrument_name", ""))
            base_asset = symbol.split("-", 1)[0].upper()
            if asset and base_asset != asset.upper():
                continue
            qty = _safe_float(getattr(position, "amount", None))
            qty_value = qty or 0.0
            positions.append(
                {
                    "ts": now,
                    "venue": self.account.venue.value,
                    "account": self.account.label,
                    "asset": base_asset,
                    "symbol": symbol,
                    "instrument_type": "option" if "PERP" not in symbol else "perp",
                    "position_side": "long" if qty_value >= 0 else "short",
                    "quantity": qty_value,
                    "entry_price": _safe_float(getattr(position, "entry_price", None)),
                    "mark_price": _safe_float(getattr(position, "mark_price", None)),
                    "unrealized_pnl": _safe_float(getattr(position, "unrealized_pnl", None)),
                    "realized_pnl": _safe_float(getattr(position, "realized_pnl", None)),
                    "notional_usd": abs(qty_value)
                    * (_safe_float(getattr(position, "mark_price", None)) or 0.0),
                    "leverage": _safe_float(getattr(position, "leverage", None)),
                    "margin_mode": str(getattr(position, "margin_type", "cross")).lower(),
                    "quality_flag": "ok",
                }
            )
        orders = [
            _execution_order_event_from_live(
                ts=now,
                venue=self.account.venue.value,
                account=self.account.label,
                asset=str(getattr(order, "instrument_name", "")).split("-", 1)[0].upper(),
                symbol=str(getattr(order, "instrument_name", "")),
                request_id=str(getattr(order, "label", "") or getattr(order, "order_id", "")),
                venue_order_id=str(getattr(order, "order_id", "")),
                status=str(getattr(order, "status", "accepted")).lower(),
                side=str(getattr(order, "direction", "")).lower(),
                order_type=str(getattr(order, "order_type", "limit")).lower(),
                time_in_force=str(getattr(order, "time_in_force", "gtc")).lower(),
                price=_safe_float(getattr(order, "limit_price", None)),
                size=_safe_float(getattr(order, "amount", None)),
                remaining_size=_safe_float(getattr(order, "remaining_amount", None)),
                filled_size=_safe_float(getattr(order, "filled_amount", None)),
                avg_fill_price=_safe_float(getattr(order, "avg_fill_price", None)),
                reason=None,
                payload=_model_dump(getattr(order, "to_dict", lambda: {})()),
                event_type="snapshot",
                quality_flag="ok",
            )
            for order in self.client.active_subaccount.orders.list_open()
        ]
        return LiveAccountSnapshot(
            balances=balances,
            positions=positions,
            orders=orders,
            health=ExecutionVenueHealth(
                venue=self.account.venue,
                account=self.account.label,
                status="online",
                ws_connected=False,
                private_ws_connected=False,
                latency_ms=None,
            ),
        )

    def request_quote(self, request: ExecutionQuoteRequest) -> LiveQuoteResponse:
        raise ValueError("Derive does not support standalone quote requests in the operator flow")

    def place_order(self, intent: ExecutionIntent) -> LiveOrderResponse:
        if intent.price is None and intent.order_type != ExecutionOrderType.MARKET:
            raise ValueError("Derive limit-style live orders require price")
        market_price = intent.price or self._market_price(intent.symbol, intent.side)
        order = self.client.active_subaccount.orders.create(
            amount=Decimal(str(intent.size)),
            direction=Direction.buy if intent.side == ExecutionOrderSide.BUY else Direction.sell,
            instrument_name=intent.symbol,
            limit_price=Decimal(str(market_price)),
            order_type=self._map_order_type(intent.order_type),
            reduce_only=intent.reduce_only,
            time_in_force=self._map_tif(intent.order_type, intent.time_in_force.value),
            label=intent.client_order_id or intent.request_id,
        )
        venue_order_id = str(getattr(order, "order_id", "")) or f"derive-{uuid4().hex[:12]}"
        return LiveOrderResponse(
            venue_order_id=venue_order_id,
            message="Live order accepted by Derive.",
            payload=_model_dump(getattr(order, "to_dict", lambda: {})()),
            status=ExecutionOrderStatus.ACCEPTED,
            filled_size=_safe_float(getattr(order, "filled_amount", None)) or 0.0,
            remaining_size=_safe_float(getattr(order, "remaining_amount", None)),
            avg_fill_price=_safe_float(getattr(order, "avg_fill_price", None)),
        )

    def cancel(self, request: ExecutionCancelRequest) -> LiveOrderResponse:
        if not request.venue_order_id or not request.symbol:
            raise ValueError("Derive cancel requires venue_order_id and symbol")
        result = self.client.active_subaccount.orders.cancel(
            order_id=request.venue_order_id,
            instrument_name=request.symbol,
        )
        return LiveOrderResponse(
            venue_order_id=str(request.venue_order_id),
            message="Live cancel accepted by Derive.",
            payload=_model_dump(getattr(result, "to_dict", lambda: {})()),
            status=ExecutionOrderStatus.CANCELED,
        )

    def cancel_all(self, request: ExecutionCancelAllRequest) -> LiveOrderResponse:
        result = self.client.active_subaccount.orders.cancel_all()
        return LiveOrderResponse(
            venue_order_id=f"derive-cancel-all-{uuid4().hex[:8]}",
            message="Live cancel-all accepted by Derive.",
            payload=_model_dump(getattr(result, "to_dict", lambda: {})()),
            status=ExecutionOrderStatus.CANCELED,
        )

    def _market_price(self, symbol: str, side: ExecutionOrderSide) -> float:
        for row in self.market_snapshots(symbol.split("-", 1)[0].upper()):
            if row["symbol"] == symbol:
                if side == ExecutionOrderSide.BUY and row["best_ask"] is not None:
                    return row["best_ask"]
                if side == ExecutionOrderSide.SELL and row["best_bid"] is not None:
                    return row["best_bid"]
                if row["mark_price"] is not None:
                    return row["mark_price"]
        raise ValueError(f"Unable to resolve market price for {symbol}")

    def _map_order_type(self, order_type: ExecutionOrderType) -> DeriveOrderType:
        if order_type == ExecutionOrderType.MARKET:
            return DeriveOrderType.market
        return DeriveOrderType.limit

    def _map_tif(self, order_type: ExecutionOrderType, time_in_force: str) -> DeriveTimeInForce:
        if order_type == ExecutionOrderType.POST_ONLY_LIMIT:
            return DeriveTimeInForce.post_only
        if order_type == ExecutionOrderType.IOC:
            return DeriveTimeInForce.ioc
        if order_type == ExecutionOrderType.FOK:
            return DeriveTimeInForce.fok
        mapping = {
            "ioc": DeriveTimeInForce.ioc,
            "fok": DeriveTimeInForce.fok,
            "post_only": DeriveTimeInForce.post_only,
            "gtc": DeriveTimeInForce.gtc,
        }
        return mapping.get(time_in_force.lower(), DeriveTimeInForce.gtc)


class HyperliquidLiveClient:
    def __init__(self, *, config: AppConfig, account: ExecutionAccountConfig) -> None:
        self.config = config
        self.account = account
        private_key = _require_env(account.private_key_env, "private_key_env", account)
        wallet: LocalAccount = Account.from_key(private_key)
        self.address = account.wallet_address or wallet.address
        self.info = HyperliquidInfo(
            base_url=config.execution.hyperliquid_api_base or hyperliquid_constants.MAINNET_API_URL,
            skip_ws=True,
            timeout=10.0,
        )
        self.exchange = HyperliquidExchange(
            wallet=wallet,
            base_url=config.execution.hyperliquid_api_base or hyperliquid_constants.MAINNET_API_URL,
            vault_address=account.vault_address,
            account_address=self.address,
            timeout=10.0,
        )

    def market_snapshots(self, asset: str | None = None) -> list[dict[str, Any]]:
        meta, contexts = self.info.meta_and_asset_ctxs()
        rows: list[dict[str, Any]] = []
        for universe_row, context_row in zip(meta["universe"], contexts, strict=False):
            name = str(universe_row["name"]).upper()
            if asset and name != asset.upper():
                continue
            rows.append(
                {
                    "venue": self.account.venue.value,
                    "asset": name,
                    "symbol": f"{name}-PERP",
                    "instrument_type": "perp",
                    "status": "online",
                    "tick_size": None,
                    "step_size": None,
                    "min_size": None,
                    "min_notional": None,
                    "best_bid": _first_float(context_row.get("impactPxs")),
                    "best_ask": _second_float(context_row.get("impactPxs")),
                    "mark_price": _safe_float(context_row.get("markPx")),
                    "index_price": _safe_float(context_row.get("oraclePx")),
                    "quality_flag": "ok",
                }
            )
        return rows

    def account_snapshot(self, asset: str | None = None) -> LiveAccountSnapshot:
        now = datetime.now(tz=timezone.utc)
        user_state = self.info.user_state(self.address)
        balances = [
            {
                "ts": now,
                "venue": self.account.venue.value,
                "account": self.account.label,
                "asset": "USDC",
                "balance_type": "total",
                "total": _safe_float(user_state.get("marginSummary", {}).get("accountValue")),
                "available": _safe_float(user_state.get("withdrawable")),
                "locked": _safe_float(user_state.get("marginSummary", {}).get("totalMarginUsed")),
                "quality_flag": "ok",
            }
        ]
        positions: list[dict[str, Any]] = []
        for position_wrapper in user_state.get("assetPositions", []):
            position = position_wrapper.get("position", {})
            symbol = f"{position.get('coin', '')}-PERP"
            base_asset = str(position.get("coin", "")).upper()
            if asset and base_asset != asset.upper():
                continue
            qty = _safe_float(position.get("szi"))
            qty_value = qty or 0.0
            position_value = _safe_float(position.get("positionValue")) or 0.0
            positions.append(
                {
                    "ts": now,
                    "venue": self.account.venue.value,
                    "account": self.account.label,
                    "asset": base_asset,
                    "symbol": symbol,
                    "instrument_type": "perp",
                    "position_side": "long" if qty_value >= 0 else "short",
                    "quantity": qty_value,
                    "entry_price": _safe_float(position.get("entryPx")),
                    "mark_price": position_value / abs(qty_value) if qty_value else None,
                    "unrealized_pnl": _safe_float(position.get("unrealizedPnl")),
                    "realized_pnl": None,
                    "notional_usd": position_value,
                    "leverage": _safe_float(position.get("leverage", {}).get("value")),
                    "margin_mode": str(position.get("leverage", {}).get("type", "cross")),
                    "quality_flag": "ok",
                }
            )
        orders: list[dict[str, Any]] = []
        for order in self.info.open_orders(self.address):
            coin = str(order.get("coin", "")).upper()
            if asset and coin != asset.upper():
                continue
            orders.append(
                _execution_order_event_from_live(
                    ts=now,
                    venue=self.account.venue.value,
                    account=self.account.label,
                    asset=coin,
                    symbol=f"{coin}-PERP",
                    request_id=str(order.get("cloid") or order.get("oid") or uuid4().hex),
                    venue_order_id=str(order.get("oid", "")),
                    status="accepted",
                    side=(
                        "buy"
                        if bool(order.get("side") == "B") or bool(order.get("isBuy"))
                        else "sell"
                    ),
                    order_type="limit",
                    time_in_force=str(order.get("tif", "gtc")).lower(),
                    price=_safe_float(order.get("limitPx")),
                    size=_safe_float(order.get("sz")),
                    remaining_size=_safe_float(order.get("sz")),
                    filled_size=0.0,
                    avg_fill_price=None,
                    reason=None,
                    payload=order,
                    event_type="snapshot",
                    quality_flag="ok",
                )
            )
        fills = [
            {
                "ts": now,
                "fill_id": str(fill.get("tid") or uuid4().hex),
                "request_id": str(fill.get("cloid") or fill.get("oid") or ""),
                "venue_order_id": str(fill.get("oid", "")),
                "venue": self.account.venue.value,
                "account": self.account.label,
                "asset": str(fill.get("coin", "")).upper(),
                "symbol": f"{str(fill.get('coin', '')).upper()}-PERP",
                "side": (
                    "buy"
                    if bool(fill.get("side") == "B")
                    or bool(fill.get("dir") == "Open Long")
                    else "sell"
                ),
                "price": _safe_float(fill.get("px")),
                "size": _safe_float(fill.get("sz")),
                "fee": _safe_float(fill.get("fee")),
                "fee_asset": str(fill.get("feeToken", "USDC")).upper(),
                "liquidity": str(fill.get("liquidity", "")).lower() or "unknown",
                "payload": fill,
                "quality_flag": "ok",
            }
            for fill in self.info.user_fills_by_time(
                self.address,
                start_time=int((now - timedelta(hours=24)).timestamp() * 1000),
            )
            if not asset or str(fill.get("coin", "")).upper() == asset.upper()
        ]
        return LiveAccountSnapshot(
            balances=balances,
            positions=positions,
            orders=orders,
            fills=fills,
            health=ExecutionVenueHealth(
                venue=self.account.venue,
                account=self.account.label,
                status="online",
                ws_connected=False,
                private_ws_connected=False,
                latency_ms=None,
            ),
        )

    def request_quote(self, request: ExecutionQuoteRequest) -> LiveQuoteResponse:
        raise ValueError("Hyperliquid does not use standalone RFQ quotes in this operator flow")

    def place_order(self, intent: ExecutionIntent) -> LiveOrderResponse:
        coin = _hyperliquid_coin(intent.symbol)
        if intent.order_type == ExecutionOrderType.MARKET:
            result = self.exchange.market_open(
                coin,
                intent.side == ExecutionOrderSide.BUY,
                intent.size,
                px=intent.price,
            )
        else:
            if intent.price is None:
                raise ValueError("Hyperliquid limit-style orders require price")
            result = self.exchange.order(
                coin,
                intent.side == ExecutionOrderSide.BUY,
                intent.size,
                intent.price,
                _hyperliquid_order_type(intent),
                reduce_only=intent.reduce_only,
            )
        venue_order_id = _hyperliquid_order_id(result)
        return LiveOrderResponse(
            venue_order_id=venue_order_id,
            message="Live order accepted by Hyperliquid.",
            payload=result,
            status=(
                ExecutionOrderStatus.ACCEPTED
                if result.get("status") == "ok"
                else ExecutionOrderStatus.REJECTED
            ),
        )

    def cancel(self, request: ExecutionCancelRequest) -> LiveOrderResponse:
        if not request.venue_order_id:
            raise ValueError("Hyperliquid cancel requires venue_order_id")
        coin = _hyperliquid_coin(request.symbol or f"{request.asset}-PERP")
        result = self.exchange.cancel(coin, int(request.venue_order_id))
        return LiveOrderResponse(
            venue_order_id=str(request.venue_order_id),
            message="Live cancel submitted to Hyperliquid.",
            payload=result,
            status=(
                ExecutionOrderStatus.CANCELED
                if result.get("status") == "ok"
                else ExecutionOrderStatus.REJECTED
            ),
        )

    def cancel_all(self, request: ExecutionCancelAllRequest) -> LiveOrderResponse:
        open_orders = self.info.open_orders(self.address)
        cancel_requests = []
        for order in open_orders:
            coin = str(order.get("coin", "")).upper()
            if request.asset and coin != request.asset.upper():
                continue
            cancel_requests.append({"coin": coin, "oid": int(order["oid"])})
        result = (
            self.exchange.bulk_cancel(cancel_requests)
            if cancel_requests
            else {"status": "ok", "response": []}
        )
        return LiveOrderResponse(
            venue_order_id=f"hyperliquid-cancel-all-{uuid4().hex[:8]}",
            message="Live cancel-all submitted to Hyperliquid.",
            payload=result,
            status=(
                ExecutionOrderStatus.CANCELED
                if result.get("status") == "ok"
                else ExecutionOrderStatus.REJECTED
            ),
        )


class LighterLiveClient:
    def __init__(self, *, config: AppConfig, account: ExecutionAccountConfig) -> None:
        self.config = config
        self.account = account
        if account.account_index is None:
            raise ValueError(f"Lighter account {account.label} missing account_index")
        api_private_key = _require_env(account.api_private_key_env, "api_private_key_env", account)
        self.account_index = account.account_index
        self.api_key_index = account.api_key_index
        self.signer = SignerClient(
            config.execution.lighter_api_base,
            account_index=account.account_index,
            api_private_keys={self.api_key_index: api_private_key},
        )
        configuration = Configuration(host=config.execution.lighter_api_base)
        self.api_client = ApiClient(configuration)
        self.order_api = OrderApi(self.api_client)
        self.account_api = AccountApi(self.api_client)

    def market_snapshots(self, asset: str | None = None) -> list[dict[str, Any]]:
        details = asyncio.run(self.order_api.order_book_details())
        rows: list[dict[str, Any]] = []
        for detail in details.order_book_details:
            base_asset = str(detail.symbol).split("-", 1)[0].upper()
            if asset and base_asset != asset.upper():
                continue
            rows.append(
                {
                    "venue": self.account.venue.value,
                    "asset": base_asset,
                    "symbol": str(detail.symbol),
                    "instrument_type": "perp",
                    "status": str(detail.status).lower(),
                    "tick_size": 10 ** (-int(detail.supported_price_decimals)),
                    "step_size": 10 ** (-int(detail.supported_size_decimals)),
                    "min_size": _safe_float(detail.min_base_amount),
                    "min_notional": _safe_float(detail.min_quote_amount),
                    "best_bid": None,
                    "best_ask": None,
                    "mark_price": _safe_float(detail.last_trade_price),
                    "index_price": None,
                    "quality_flag": "ok",
                }
            )
        return rows

    def account_snapshot(self, asset: str | None = None) -> LiveAccountSnapshot:
        now = datetime.now(tz=timezone.utc)
        details = asyncio.run(self.account_api.account(by="index", value=str(self.account_index)))
        account = details.accounts[0]
        balances = [
            {
                "ts": now,
                "venue": self.account.venue.value,
                "account": self.account.label,
                "asset": str(item.symbol).upper(),
                "balance_type": "total",
                "total": _safe_float(item.balance),
                "available": (_safe_float(item.balance) or 0.0)
                - (_safe_float(item.locked_balance) or 0.0),
                "locked": _safe_float(item.locked_balance),
                "quality_flag": "ok",
            }
            for item in account.assets
            if not asset or str(item.symbol).upper() == asset.upper()
        ]
        positions: list[dict[str, Any]] = []
        for item in account.positions:
            base_asset = str(item.symbol).split("-", 1)[0].upper()
            if asset and base_asset != asset.upper():
                continue
            qty_value = _safe_float(item.position) or 0.0
            position_value = _safe_float(item.position_value) or 0.0
            positions.append(
                {
                    "ts": now,
                    "venue": self.account.venue.value,
                    "account": self.account.label,
                    "asset": base_asset,
                    "symbol": str(item.symbol),
                    "instrument_type": "perp",
                    "position_side": "long" if int(item.sign) >= 0 else "short",
                    "quantity": qty_value,
                    "entry_price": _safe_float(item.avg_entry_price),
                    "mark_price": position_value / abs(qty_value) if qty_value else None,
                    "unrealized_pnl": _safe_float(item.unrealized_pnl),
                    "realized_pnl": _safe_float(item.realized_pnl),
                    "notional_usd": position_value,
                    "leverage": None,
                    "margin_mode": "isolated" if int(item.margin_mode) else "cross",
                    "quality_flag": "ok",
                }
            )
        auth = self._auth_token()
        orders: list[dict[str, Any]] = []
        for market_id, symbol in self._market_map().items():
            if asset and symbol.split("-", 1)[0].upper() != asset.upper():
                continue
            active_orders = asyncio.run(
                self.order_api.account_active_orders(
                    self.account_index,
                    market_id,
                    auth=auth,
                )
            )
            for order in active_orders.orders:
                orders.append(
                    _execution_order_event_from_live(
                        ts=now,
                        venue=self.account.venue.value,
                        account=self.account.label,
                        asset=symbol.split("-", 1)[0].upper(),
                        symbol=symbol,
                        request_id=str(getattr(order, "client_order_index", "")),
                        venue_order_id=f"{market_id}:{getattr(order, 'order_index', '')}",
                        status="accepted",
                        side="sell" if bool(getattr(order, "is_ask", False)) else "buy",
                        order_type="limit",
                        time_in_force="gtc",
                        price=_safe_float(getattr(order, "price", None)),
                        size=_safe_float(getattr(order, "amount", None)),
                        remaining_size=_safe_float(getattr(order, "remaining_amount", None)),
                        filled_size=_safe_float(getattr(order, "filled_amount", None)),
                        avg_fill_price=_safe_float(getattr(order, "avg_execution_price", None)),
                        reason=None,
                        payload=_model_dump(getattr(order, "to_dict", lambda: {})()),
                        event_type="snapshot",
                        quality_flag="ok",
                    )
                )
        return LiveAccountSnapshot(
            balances=balances,
            positions=positions,
            orders=orders,
            health=ExecutionVenueHealth(
                venue=self.account.venue,
                account=self.account.label,
                status="online",
                ws_connected=False,
                private_ws_connected=False,
            ),
        )

    def request_quote(self, request: ExecutionQuoteRequest) -> LiveQuoteResponse:
        raise ValueError("Lighter does not use standalone RFQ quotes in this operator flow")

    def place_order(self, intent: ExecutionIntent) -> LiveOrderResponse:
        market_id = self._market_id(intent.symbol)
        client_order_index = int(uuid4().hex[:8], 16)
        order_type = (
            SignerClient.ORDER_TYPE_MARKET
            if intent.order_type == ExecutionOrderType.MARKET
            else SignerClient.ORDER_TYPE_LIMIT
        )
        tif = _lighter_tif(intent)
        tx, response, error = asyncio.run(
            self.signer.create_order(
                market_id,
                client_order_index,
                intent.size,
                intent.price or 0.0,
                intent.side == ExecutionOrderSide.SELL,
                order_type,
                tif,
                reduce_only=intent.reduce_only,
                api_key_index=self.api_key_index,
            )
        )
        if error:
            raise ValueError(error)
        venue_order_id = f"{market_id}:{client_order_index}"
        return LiveOrderResponse(
            venue_order_id=venue_order_id,
            message="Live order submitted to Lighter.",
            payload={
                "tx": _model_dump(getattr(tx, "to_dict", lambda: {})()),
                "response": _model_dump(getattr(response, "to_dict", lambda: {})()),
            },
            status=ExecutionOrderStatus.ACCEPTED,
        )

    def cancel(self, request: ExecutionCancelRequest) -> LiveOrderResponse:
        if not request.venue_order_id:
            raise ValueError("Lighter cancel requires venue_order_id")
        market_id, order_index = request.venue_order_id.split(":", 1)
        tx, response, error = asyncio.run(
            self.signer.cancel_order(
                int(market_id),
                int(order_index),
                api_key_index=self.api_key_index,
            )
        )
        if error:
            raise ValueError(error)
        return LiveOrderResponse(
            venue_order_id=request.venue_order_id,
            message="Live cancel submitted to Lighter.",
            payload={
                "tx": _model_dump(getattr(tx, "to_dict", lambda: {})()),
                "response": _model_dump(getattr(response, "to_dict", lambda: {})()),
            },
            status=ExecutionOrderStatus.CANCELED,
        )

    def cancel_all(self, request: ExecutionCancelAllRequest) -> LiveOrderResponse:
        tx, response, error = asyncio.run(
            self.signer.cancel_all_orders(
                SignerClient.CANCEL_ALL_TIF_IMMEDIATE,
                int(datetime.now(tz=timezone.utc).timestamp() * 1000),
                api_key_index=self.api_key_index,
            )
        )
        if error:
            raise ValueError(error)
        return LiveOrderResponse(
            venue_order_id=f"lighter-cancel-all-{uuid4().hex[:8]}",
            message="Live cancel-all submitted to Lighter.",
            payload={
                "tx": _model_dump(getattr(tx, "to_dict", lambda: {})()),
                "response": _model_dump(getattr(response, "to_dict", lambda: {})()),
            },
            status=ExecutionOrderStatus.CANCELED,
        )

    def _auth_token(self) -> str:
        auth, error = self.signer.create_auth_token_with_expiry(api_key_index=self.api_key_index)
        if error:
            raise ValueError(error)
        return auth

    def _market_map(self) -> dict[int, str]:
        details = asyncio.run(self.order_api.order_book_details())
        return {int(item.market_id): str(item.symbol) for item in details.order_book_details}

    def _market_id(self, symbol: str) -> int:
        for market_id, market_symbol in self._market_map().items():
            if market_symbol.upper() == symbol.upper():
                return market_id
        raise ValueError(f"Unknown Lighter market {symbol}")


class BebopLiveClient:
    def __init__(self, *, config: AppConfig, account: ExecutionAccountConfig) -> None:
        self.config = config
        self.account = account
        self.client = httpx.Client(base_url=config.execution.bebop_api_base, timeout=10.0)

    def market_snapshots(self, asset: str | None = None) -> list[dict[str, Any]]:
        rows: list[dict[str, Any]] = []
        assets = [asset.upper()] if asset else self.account.enabled_assets
        now_price = None
        for item in assets:
            rows.append(
                {
                    "venue": self.account.venue.value,
                    "asset": item.upper(),
                    "symbol": f"{item.upper()}-USD",
                    "instrument_type": "spot",
                    "status": "quote_only",
                    "tick_size": None,
                    "step_size": None,
                    "min_size": None,
                    "min_notional": None,
                    "best_bid": now_price,
                    "best_ask": now_price,
                    "mark_price": now_price,
                    "index_price": None,
                    "quality_flag": "partial_context",
                }
            )
        return rows

    def account_snapshot(self, asset: str | None = None) -> LiveAccountSnapshot:
        return LiveAccountSnapshot(
            health=ExecutionVenueHealth(
                venue=self.account.venue,
                account=self.account.label,
                status="quote_only",
                ws_connected=False,
                private_ws_connected=False,
            )
        )

    def request_quote(self, request: ExecutionQuoteRequest) -> LiveQuoteResponse:
        params = self._quote_params(request)
        response = self.client.get(self._quote_path(request), params=params)
        response.raise_for_status()
        payload = response.json()
        quote_id = str(payload.get("quoteId") or payload.get("quote_id") or uuid4().hex)
        price = _safe_float(payload.get("price") or payload.get("buyPrice"))
        expires_at = _expires_at_from_payload(payload)
        return LiveQuoteResponse(
            quote_id=quote_id,
            message="Live Bebop quote received.",
            payload=payload,
            price=price,
            expires_at=expires_at,
            route=str(payload.get("route") or "router"),
        )

    def place_order(self, intent: ExecutionIntent) -> LiveOrderResponse:
        if not intent.quote_id:
            raise ValueError("Bebop live execute requires a quote_id")
        metadata = {**self.account.metadata, **intent.metadata}
        quote_payload = cast(dict[str, Any], metadata.get("quote_payload") or {})
        if not quote_payload:
            raise ValueError("Bebop live execute requires quote_payload metadata")
        expires_at = _expires_at_from_payload(quote_payload)
        if expires_at is not None and expires_at <= datetime.now(tz=timezone.utc):
            raise ValueError("Bebop quote expired before execution")

        execution_mode = str(metadata.get("execution_mode") or "self_execute").lower()
        if execution_mode == "gasless":
            response = self.client.post(
                str(metadata.get("order_path") or "/router/ethereum/v1/order"),
                json=self._gasless_order_body(intent, quote_payload, metadata),
            )
            response.raise_for_status()
            payload = response.json()
            venue_order_id = str(
                payload.get("orderId")
                or payload.get("order_id")
                or payload.get("txHash")
                or payload.get("transactionHash")
                or f"bebop-{intent.quote_id}"
            )
            return LiveOrderResponse(
                venue_order_id=venue_order_id,
                message="Bebop gasless execution accepted.",
                payload=payload,
                status=ExecutionOrderStatus.ACCEPTED,
            )

        rpc_url = _require_env(self.account.rpc_url_env, "rpc_url_env", self.account)
        private_key = _require_env(self.account.private_key_env, "private_key_env", self.account)
        tx_bundle = _bebop_transaction_bundle(quote_payload)
        if not tx_bundle:
            raise ValueError("Bebop quote payload did not include executable transaction data")
        tx_hashes = [
            _sign_and_send_transaction(
                tx=tx,
                private_key=private_key,
                rpc_url=rpc_url,
                default_from=(
                    self.account.wallet_address or _address_from_key(self.account.private_key_env)
                ),
                default_chain_id=_safe_int(metadata.get("chain_id")) or 1,
            )
            for tx in tx_bundle
        ]
        payload = {
            "quote_id": intent.quote_id,
            "execution_mode": "self_execute",
            "transaction_hashes": tx_hashes,
            "quote_payload": quote_payload,
        }
        return LiveOrderResponse(
            venue_order_id=tx_hashes[-1],
            message="Bebop self-execution transaction bundle broadcast.",
            payload=payload,
            status=ExecutionOrderStatus.ACCEPTED,
        )

    def cancel(self, request: ExecutionCancelRequest) -> LiveOrderResponse:
        return LiveOrderResponse(
            venue_order_id=request.venue_order_id or f"bebop-cancel-{uuid4().hex[:8]}",
            message="Bebop quotes expire naturally; no live cancel request was sent.",
            payload=request.model_dump(mode="json"),
            status=ExecutionOrderStatus.CANCELED,
        )

    def cancel_all(self, request: ExecutionCancelAllRequest) -> LiveOrderResponse:
        return LiveOrderResponse(
            venue_order_id=f"bebop-cancel-all-{uuid4().hex[:8]}",
            message="Bebop has no standing-order cancel-all in router mode.",
            payload=request.model_dump(mode="json"),
            status=ExecutionOrderStatus.CANCELED,
        )

    def _quote_path(self, request: ExecutionQuoteRequest) -> str:
        metadata = {**self.account.metadata, **request.metadata}
        return str(metadata.get("quote_path") or "/router/ethereum/v1/quote")

    def _quote_params(self, request: ExecutionQuoteRequest) -> dict[str, Any]:
        metadata = {**self.account.metadata, **request.metadata}
        sell_amount = metadata.get("sell_amount")
        buy_amount = metadata.get("buy_amount")
        if sell_amount is None and buy_amount is None:
            sell_amount = str(int(request.size * 10**18))
        params = {
            "sell_tokens": metadata.get("sell_token"),
            "buy_tokens": metadata.get("buy_token"),
            "sell_amounts": sell_amount,
            "buy_amounts": buy_amount,
            "taker_address": self.account.wallet_address,
        }
        cleaned = {key: value for key, value in params.items() if value not in {None, ""}}
        missing = {"sell_tokens", "buy_tokens", "taker_address"} - set(cleaned)
        if missing:
            raise ValueError(f"Bebop quote requires metadata for {', '.join(sorted(missing))}")
        return cleaned

    def _gasless_order_body(
        self,
        intent: ExecutionIntent,
        quote_payload: dict[str, Any],
        metadata: dict[str, Any],
    ) -> dict[str, Any]:
        body = dict(metadata.get("order_payload") or {})
        body.setdefault("quoteId", intent.quote_id)
        body.setdefault("quote_id", intent.quote_id)
        body.setdefault("takerAddress", self.account.wallet_address)
        if "quote" in quote_payload:
            body.setdefault("quote", quote_payload["quote"])
        if metadata.get("signature") is not None:
            body.setdefault("signature", metadata["signature"])
        if metadata.get("approval_signature") is not None:
            body.setdefault("approvalSignature", metadata["approval_signature"])
        return body


def _require_env(name: str | None, field_name: str, account: ExecutionAccountConfig) -> str:
    if not name:
        raise ValueError(f"Execution account {account.label} missing {field_name}")
    value = os.getenv(name)
    if not value:
        raise ValueError(f"Execution account {account.label} missing env var {name}")
    return value


def _address_from_key(env_name: str | None) -> str | None:
    if not env_name:
        return None
    private_key = os.getenv(env_name)
    if not private_key:
        return None
    return Account.from_key(private_key).address


def _safe_float(value: Any) -> float | None:
    if value in {None, ""}:
        return None
    try:
        return float(value)
    except (TypeError, ValueError):
        return None


def _safe_int(value: Any) -> int | None:
    if value in {None, ""}:
        return None
    try:
        if isinstance(value, str) and value.startswith("0x"):
            return int(value, 16)
        return int(value)
    except (TypeError, ValueError):
        return None


def _first_float(values: Any) -> float | None:
    if isinstance(values, (list, tuple)) and values:
        return _safe_float(values[0])
    return None


def _second_float(values: Any) -> float | None:
    if isinstance(values, (list, tuple)) and len(values) > 1:
        return _safe_float(values[1])
    return None


def _model_dump(value: Any) -> dict[str, Any]:
    if isinstance(value, dict):
        return value
    if hasattr(value, "model_dump"):
        return value.model_dump(mode="json")
    return {}


def _execution_order_event_from_live(
    *,
    ts: datetime,
    venue: str,
    account: str,
    asset: str,
    symbol: str,
    request_id: str,
    venue_order_id: str,
    status: str,
    side: str | None,
    order_type: str | None,
    time_in_force: str | None,
    price: float | None,
    size: float | None,
    remaining_size: float | None,
    filled_size: float | None,
    avg_fill_price: float | None,
    reason: str | None,
    payload: dict[str, Any],
    event_type: str,
    quality_flag: str,
) -> dict[str, Any]:
    return {
        "ts": ts,
        "venue": venue,
        "account": account,
        "asset": asset,
        "symbol": symbol,
        "request_id": request_id,
        "venue_order_id": venue_order_id,
        "client_order_id": None,
        "event_type": event_type,
        "status": status,
        "side": side,
        "order_type": order_type,
        "time_in_force": time_in_force,
        "price": price,
        "size": size,
        "remaining_size": remaining_size,
        "filled_size": filled_size,
        "avg_fill_price": avg_fill_price,
        "reason": reason,
        "payload": payload,
        "quality_flag": quality_flag,
    }


def _hyperliquid_coin(symbol: str) -> str:
    return symbol.upper().replace("-PERP", "").replace("-USD", "")


def _hyperliquid_order_type(intent: ExecutionIntent) -> dict[str, Any]:
    if intent.order_type == ExecutionOrderType.POST_ONLY_LIMIT:
        return {"limit": {"tif": "Alo"}}
    if intent.order_type == ExecutionOrderType.IOC:
        return {"limit": {"tif": "Ioc"}}
    if intent.order_type == ExecutionOrderType.FOK:
        return {"limit": {"tif": "Ioc"}}
    return {"limit": {"tif": "Gtc"}}


def _hyperliquid_order_id(result: dict[str, Any]) -> str:
    statuses = (
        result.get("response", {})
        .get("data", {})
        .get("statuses", [])
    )
    for item in statuses:
        if "resting" in item and item["resting"].get("oid") is not None:
            return str(item["resting"]["oid"])
        if "filled" in item and item["filled"].get("oid") is not None:
            return str(item["filled"]["oid"])
    return f"hyperliquid-{uuid4().hex[:12]}"


def _lighter_tif(intent: ExecutionIntent) -> int:
    if intent.order_type == ExecutionOrderType.POST_ONLY_LIMIT:
        return SignerClient.ORDER_TIME_IN_FORCE_POST_ONLY
    if intent.order_type == ExecutionOrderType.IOC:
        return SignerClient.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL
    if intent.order_type == ExecutionOrderType.FOK:
        return SignerClient.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL
    return SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME


def _expires_at_from_payload(payload: dict[str, Any]) -> datetime | None:
    for key in ("expiry", "expiryTime", "expiresAt", "expiration"):
        value = payload.get(key)
        if value is None:
            continue
        if isinstance(value, (int, float)):
            if value > 10_000_000_000:
                return datetime.fromtimestamp(value / 1000, tz=timezone.utc)
            return datetime.fromtimestamp(value, tz=timezone.utc)
        if isinstance(value, str):
            try:
                return datetime.fromisoformat(value.replace("Z", "+00:00"))
            except ValueError:
                continue
    return datetime.now(tz=timezone.utc) + timedelta(hours=1)


def _bebop_transaction_bundle(payload: dict[str, Any]) -> list[dict[str, Any]]:
    bundle: list[dict[str, Any]] = []
    for key in ("approvalTxs", "approval_txs"):
        raw = payload.get(key)
        if isinstance(raw, list):
            for item in raw:
                tx = _normalize_transaction_payload(item)
                if tx:
                    bundle.append(tx)
    for key in ("approvalTx", "approval_tx", "tx", "transaction"):
        tx = _normalize_transaction_payload(payload.get(key))
        if tx:
            bundle.append(tx)
    direct_tx = _normalize_transaction_payload(payload)
    if direct_tx and direct_tx not in bundle:
        bundle.append(direct_tx)
    return bundle


def _normalize_transaction_payload(value: Any) -> dict[str, Any] | None:
    if not isinstance(value, dict):
        return None
    if value.get("to") is None and value.get("data") is None:
        return None
    return value


def _rpc_call(rpc_url: str, method: str, params: list[Any]) -> Any:
    with httpx.Client(timeout=10.0) as client:
        response = client.post(
            rpc_url,
            json={
                "jsonrpc": "2.0",
                "id": 1,
                "method": method,
                "params": params,
            },
        )
    response.raise_for_status()
    payload = response.json()
    if payload.get("error") is not None:
        raise ValueError(f"RPC {method} failed: {payload['error']}")
    return payload.get("result")


def _sign_and_send_transaction(
    *,
    tx: dict[str, Any],
    private_key: str,
    rpc_url: str,
    default_from: str | None,
    default_chain_id: int,
) -> str:
    account: LocalAccount = Account.from_key(private_key)
    tx_from = tx.get("from")
    from_address = str(
        tx_from
        if isinstance(tx_from, str) and tx_from.lower() == account.address.lower()
        else account.address
    )
    nonce = _safe_int(tx.get("nonce"))
    if nonce is None:
        nonce_result = _rpc_call(rpc_url, "eth_getTransactionCount", [from_address, "pending"])
        nonce = _safe_int(nonce_result)
    gas = _safe_int(tx.get("gas"))
    if gas is None:
        gas_result = _rpc_call(
            rpc_url,
            "eth_estimateGas",
            [
                {
                    "from": from_address,
                    "to": tx.get("to"),
                    "data": tx.get("data", "0x"),
                    "value": tx.get("value", "0x0"),
                }
            ],
        )
        gas = _safe_int(gas_result)
    prepared: dict[str, Any] = {
        "to": tx.get("to"),
        "data": tx.get("data", "0x"),
        "value": _safe_int(tx.get("value")) or 0,
        "nonce": nonce or 0,
        "gas": gas or 0,
        "chainId": _safe_int(tx.get("chainId")) or default_chain_id,
    }
    max_fee = _safe_int(tx.get("maxFeePerGas"))
    max_priority = _safe_int(tx.get("maxPriorityFeePerGas"))
    gas_price = _safe_int(tx.get("gasPrice"))
    if max_fee is not None and max_priority is not None:
        prepared["maxFeePerGas"] = max_fee
        prepared["maxPriorityFeePerGas"] = max_priority
        prepared["type"] = 2
    elif gas_price is not None:
        prepared["gasPrice"] = gas_price
    else:
        gas_price_result = _rpc_call(rpc_url, "eth_gasPrice", [])
        prepared["gasPrice"] = _safe_int(gas_price_result) or 0
    signed = account.sign_transaction(prepared)
    tx_hash = _rpc_call(
        rpc_url,
        "eth_sendRawTransaction",
        [signed.raw_transaction.hex()],
    )
    if not isinstance(tx_hash, str):
        raise ValueError("RPC eth_sendRawTransaction did not return a transaction hash")
    return tx_hash
