"""
Hyperliquid execution provider — live and paper trading via NautilusTrader.

Live mode: Submits real orders to Hyperliquid testnet/mainnet via REST.
Paper mode: Tracks virtual positions, simulates fills with realistic slippage.

Uses the hyperliquid-python-sdk for signed order submission.
"""
from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass, field

import requests

from nautilus_trader.model.enums import OrderSide, OrderType, TimeInForce
from nautilus_trader.model.identifiers import ClientOrderId, InstrumentId, VenueOrderId
from nautilus_trader.model.objects import Price, Quantity

logger = logging.getLogger(__name__)

TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
MAINNET_API = "https://api.hyperliquid.xyz/info"


@dataclass
class SimulatedPosition:
    coin: str
    quantity: float
    entry_price: float
    side: str          # BUY or SELL
    fee_paid: float = 0.0
    pnl: float = 0.0


@dataclass
class SimulatedOrder:
    cloid: str
    coin: str
    side: str
    quantity: float
    price: float
    timestamp: float = field(default_factory=time.time)
    filled: bool = False
    fill_price: float = 0.0
    fee: float = 0.0
    pnl: float = 0.0


class HyperliquidExecutionProvider:
    """Live trading via Hyperliquid SDK + REST API."""

    def __init__(
        self,
        private_key: str,
        testnet: bool = True,
        vault_address: str | None = None,
    ):
        self._pk = private_key
        self._vault = vault_address
        self._testnet = testnet
        self._api_url = TESTNET_API if testnet else MAINNET_API
        self._exchange = None
        self._info = None
        self._address: str | None = None

    def _ensure_sdk(self):
        if self._exchange is None:
            from hyperliquid.exchange import Exchange
            from hyperliquid.info import Info

            self._info = Info(self._api_url, skip_ws=True)
            self._exchange = Exchange(
                wallet=self._info,
                private_key=self._pk,
                vault_address=self._vault,
                account_address=None,
                is_testnet=self._testnet,
            )
            meta = self._info.meta()
            if meta and "universe" in meta:
                logger.info("HL SDK initialized: %d assets", len(meta.get("universe", [])))

    @property
    def address(self) -> str | None:
        if not self._address:
            self._ensure_sdk()
            if self._exchange:
                self._address = self._exchange.wallet.address
        return self._address

    def submit_limit_order(
        self,
        coin: str,
        side: str,        # "BUY" or "SELL"
        size: float,
        price: float,
        post_only: bool = True,
        reduce_only: bool = False,
    ) -> dict | None:
        """Submit a limit order. Returns order response or None on failure."""
        self._ensure_sdk()
        try:
            is_buy = side.upper() == "BUY"
            result = self._exchange.order(
                name=coin,
                is_buy=is_buy,
                sz=size,
                limit_px=price,
                order_type={"limit": {"tif": "Gtc" if post_only else "Ioc"}},
                reduce_only=reduce_only,
            )
            logger.info("Order submitted: %s %s %.6f @ %.1f → %s",
                         side, coin, size, price, result)
            return result
        except Exception as e:
            logger.error("Order failed: %s %s: %s", side, coin, e)
            return None

    def cancel_order(self, coin: str, cloid: str) -> bool:
        """Cancel an order by client order ID."""
        self._ensure_sdk()
        try:
            self._exchange.cancel(coin, cloid)
            return True
        except Exception as e:
            logger.warning("Cancel failed for %s/%s: %s", coin, cloid, e)
            return False

    def cancel_all(self, coin: str | None = None):
        """Cancel all open orders, optionally filtered by coin."""
        self._ensure_sdk()
        try:
            self._exchange.cancel_all(coin)
        except Exception as e:
            logger.warning("Cancel all failed: %s", e)

    def get_positions(self) -> list[dict]:
        """Get open positions for the wallet."""
        if not self.address:
            return []
        resp = requests.post(
            self._api_url,
            json={"type": "clearinghouseState", "user": self.address},
            timeout=10,
        )
        if resp.status_code != 200:
            return []
        data = resp.json()
        positions = []
        for pos in data.get("assetPositions", []):
            pos_type = pos.get("position", {})
            if pos_type:
                coin = pos_type.get("coin", "")
                szi = float(pos_type.get("szi", 0))
                if coin and abs(szi) > 0:
                    positions.append({
                        "coin": coin,
                        "size": szi,
                        "entry_px": float(pos_type.get("entryPx", 0)),
                        "unrealized_pnl": float(pos_type.get("unrealizedPnl", 0)),
                    })
        return positions

    def get_open_orders(self) -> list[dict]:
        if not self.address:
            return []
        resp = requests.post(
            self._api_url,
            json={"type": "openOrders", "user": self.address},
            timeout=10,
        )
        if resp.status_code != 200:
            return []
        return resp.json()


class PaperExecutionProvider:
    """Paper trading — simulated fills against real Hyperliquid mark prices."""

    def __init__(
        self,
        maker_fee: float = 0.0002,
        taker_fee: float = 0.0005,
        slippage_bps: float = 1.0,
    ):
        self.maker_fee = maker_fee
        self.taker_fee = taker_fee
        self.slippage_bps = slippage_bps

        self.positions: dict[str, SimulatedPosition] = {}
        self.orders: dict[str, SimulatedOrder] = {}
        self.trades: list[dict] = []
        self._counter = 0

    def submit(
        self,
        coin: str,
        side: str,
        size: float,
        price: float,
        fee_model: str = "taker",
        mark_price: float | None = None,
    ) -> str:
        """Submit a simulated order. Returns client order ID."""
        self._counter += 1
        cloid = f"paper-{self._counter}"

        order = SimulatedOrder(cloid=cloid, coin=coin, side=side, quantity=size, price=price)
        self.orders[cloid] = order

        # Simulate immediate fill at mark price or limit price
        fill_price = mark_price if mark_price and mark_price > 0 else price
        fee_rate = self.maker_fee if fee_model == "maker" else self.taker_fee

        # Apply slippage
        slip = fill_price * self.slippage_bps / 10000
        effective_px = fill_price + slip if side.upper() == "BUY" else fill_price - slip

        fee = size * effective_px * fee_rate
        order.filled = True
        order.fill_price = effective_px
        order.fee = fee

        # Update position
        pos = self.positions.get(coin)
        if pos and pos.side != side:
            # Closing trade — calculate PnL
            pnl = (effective_px - pos.entry_price) * min(size, abs(pos.quantity))
            if pos.side == "SELL":
                pnl = -pnl
            order.pnl = pnl
            pos.quantity -= size
            pos.fee_paid += fee
            pos.pnl += pnl
            if abs(pos.quantity) < 1e-8:
                del self.positions[coin]
        else:
            # Opening or adding to position
            if coin not in self.positions:
                self.positions[coin] = SimulatedPosition(
                    coin=coin, quantity=size, entry_price=effective_px, side=side
                )
            else:
                pos.quantity += size
                pos.entry_price = (pos.entry_price * (pos.quantity - size) + effective_px * size) / pos.quantity

        trade = {
            "cloid": cloid,
            "coin": coin,
            "side": side,
            "size": size,
            "price": effective_px,
            "fee": round(fee, 6),
            "pnl": round(order.pnl, 4),
            "timestamp": time.time(),
        }
        self.trades.append(trade)
        logger.debug("Paper fill: %s %s %.6f @ %.1f | pnl=%.4f fee=%.6f",
                      side, coin, size, effective_px, order.pnl, fee)
        return cloid

    def cancel(self, cloid: str) -> bool:
        if cloid in self.orders and not self.orders[cloid].filled:
            del self.orders[cloid]
            return True
        return False

    def get_pnl(self) -> float:
        return sum(p.pnl for p in self.positions.values()) + sum(
            t.get("pnl", 0) for t in self.trades if t.get("pnl", 0) > 0
        )
