"""Live execution adapter for V2.

This file is intentionally separate from V1. It imports V1's stable
Hyperliquid executor as a dependency, but it is not a symlink and must never
write into /root/hyperliquid-bb-bot.
"""

from __future__ import annotations

import logging
import re
import sys
import time
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Optional

import requests
from hyperliquid.utils import constants

if "/root/hyperliquid-bb-bot" not in sys.path:
    sys.path.insert(0, "/root/hyperliquid-bb-bot")

from hl_bb_bot.execution import HyperliquidSdkExecutor, OrderIntent, build_managed_order_plan
from hl_bb_bot.models import TradePlan as V1TradePlan

log = logging.getLogger(__name__)
INFO_URL = "https://api.hyperliquid.xyz/info"
CLOID_RE = re.compile(r"^0x[0-9a-fA-F]{32}$")


@dataclass
class LiveOrder:
    coin: str
    is_buy: bool
    sz: float
    px: float
    sl_px: float
    tp_px: float
    leverage: int = 10
    margin_usd: float = 0.0
    entry_tif: str = "Gtc"
    cloid: Optional[str] = None


def is_valid_cloid(value: str | None) -> bool:
    return bool(value and CLOID_RE.fullmatch(str(value)))


def require_valid_cloid(value: str | None, *, role: str = "cloid") -> str:
    if not is_valid_cloid(value):
        raise ValueError(f"{role} must be a 128-bit hex string like 0x1234...; got {value!r}")
    return str(value)


def parse_usdc_balance(state: dict[str, Any]) -> dict[str, float]:
    """Parse Hyperliquid spot USDC balance defensively.

    `tokenToAvailableAfterMaintenance` is preferred when present because it
    reflects maintenance margin. Malformed rows are ignored instead of crashing
    the live loop.
    """
    total = 0.0
    hold = 0.0
    for balance in state.get("balances", []) or []:
        if balance.get("coin") == "USDC":
            total = _safe_float(balance.get("total"))
            hold = _safe_float(balance.get("hold"))
            break

    available = max(total - hold, 0.0)
    for row in state.get("tokenToAvailableAfterMaintenance", []) or []:
        if not isinstance(row, (list, tuple)) or len(row) != 2:
            continue
        token, amount = row
        try:
            token_id = int(token)
        except (TypeError, ValueError):
            continue
        if token_id == 0:
            available = max(_safe_float(amount), 0.0)
            break
    return {"total": total, "hold": hold, "available": available}


def _safe_float(value: Any) -> float:
    try:
        return float(value or 0.0)
    except (TypeError, ValueError):
        return 0.0


def _round_to_decimals(value: float, decimals: int) -> float:
    quant = Decimal("1") if decimals <= 0 else Decimal("1").scaleb(-decimals)
    rounded = Decimal(str(float(value))).quantize(quant, rounding=ROUND_HALF_UP)
    return float(rounded)


class Executor:
    """Small V2 wrapper around V1's tested HyperliquidSdkExecutor."""

    def __init__(
        self,
        private_key: str,
        account_address: Optional[str] = None,
        vault_address: Optional[str] = None,
    ) -> None:
        self.v1 = HyperliquidSdkExecutor(
            private_key=private_key,
            base_url=constants.MAINNET_API_URL,
            account_address=account_address,
            vault_address=vault_address,
        )
        self.exchange = self.v1.exchange
        self.account_address = account_address or self.v1.account_address
        log.info("Executor initialized for %s...%s", self.account_address[:10], self.account_address[-6:])

    def _post_info(self, payload: dict[str, Any]) -> Any:
        response = requests.post(INFO_URL, json=payload, timeout=12)
        response.raise_for_status()
        return response.json()

    def get_account_value(self) -> float:
        state = self._post_info({"type": "clearinghouseState", "user": self.account_address})
        return float(state.get("marginSummary", {}).get("accountValue") or 0.0)

    def get_usdc_balance(self) -> dict[str, float]:
        state = self._post_info({"type": "spotClearinghouseState", "user": self.account_address})
        return parse_usdc_balance(state)

    def get_position(self, coin: str) -> Optional[dict[str, Any]]:
        state = self._post_info({"type": "clearinghouseState", "user": self.account_address})
        for asset_pos in state.get("assetPositions", []):
            position = asset_pos.get("position", {})
            if position.get("coin") == coin and float(position.get("szi") or 0.0) != 0.0:
                return {
                    "coin": coin,
                    "szi": float(position.get("szi") or 0.0),
                    "entryPx": float(position.get("entryPx") or 0.0),
                    "positionValue": float(position.get("positionValue") or 0.0),
                    "unrealizedPnl": float(position.get("unrealizedPnl") or 0.0),
                    "leverage": int(position.get("leverage", {}).get("value") or 0),
                    "liquidationPx": float(position.get("liquidationPx") or 0.0),
                }
        return None

    def open_positions(self) -> list[dict[str, Any]]:
        state = self._post_info({"type": "clearinghouseState", "user": self.account_address})
        positions = []
        for asset_pos in state.get("assetPositions", []):
            position = asset_pos.get("position", {})
            if float(position.get("szi") or 0.0) != 0.0:
                positions.append(position)
        return positions

    def open_orders(self) -> list[dict[str, Any]]:
        orders = self._post_info({"type": "openOrders", "user": self.account_address})
        return orders if isinstance(orders, list) else []

    def frontend_open_orders(self) -> list[dict[str, Any]]:
        orders = self._post_info({"type": "frontendOpenOrders", "user": self.account_address})
        return orders if isinstance(orders, list) else []

    def user_fills(self) -> list[dict[str, Any]]:
        fills = self._post_info({"type": "userFills", "user": self.account_address})
        return fills if isinstance(fills, list) else []

    def ensure_perp_funded(self, min_perp_usd: float = 10.0) -> None:
        account_value = self.get_account_value()
        usdc = self.get_usdc_balance()
        if usdc["available"] < min_perp_usd:
            log.warning(
                "USDC available low: available=$%.2f total=$%.2f hold=$%.2f required=$%.2f",
                usdc["available"],
                usdc["total"],
                usdc["hold"],
                min_perp_usd,
            )
        else:
            log.info(
                "USDC available=$%.2f total=$%.2f hold=$%.2f isolatedMargin=$%.2f",
                usdc["available"],
                usdc["total"],
                usdc["hold"],
                account_value,
            )

    def set_leverage(self, coin: str, leverage: int, is_cross: bool = False) -> Any:
        return self.exchange.update_leverage(int(leverage), coin, is_cross=is_cross)

    def _round_price(self, coin: str, price: float) -> float:
        """Round perp prices to a conservative Hyperliquid-safe precision."""
        info = self.exchange.info
        hl_coin = info.name_to_coin.get(coin, coin)
        asset = info.coin_to_asset[hl_coin]
        sz_decimals = info.asset_to_sz_decimals[asset]
        price = float(price)
        if price <= 0:
            raise ValueError(f"price must be positive, got {price}")
        if price >= 1000:
            return _round_to_decimals(price, 0)

        digits_before_decimal = len(str(int(abs(price)))) if abs(price) >= 1 else 0
        max_decimals_by_sig_figs = max(4 - digits_before_decimal, 0)
        max_decimals_by_meta = max(6 - int(sz_decimals), 0)
        decimals = min(max_decimals_by_sig_figs, max_decimals_by_meta)
        return _round_to_decimals(price, decimals)

    def place_order(self, order: LiveOrder) -> dict[str, Any]:
        side = "long" if order.is_buy else "short"
        entry_px = self._round_price(order.coin, order.px)
        stop_px = self._round_price(order.coin, order.sl_px)
        target_px = self._round_price(order.coin, order.tp_px)
        if order.is_buy:
            if not stop_px < entry_px < target_px:
                raise ValueError(
                    f"rounded long prices invalid: stop={stop_px} entry={entry_px} target={target_px}"
                )
        else:
            if not target_px < entry_px < stop_px:
                raise ValueError(
                    f"rounded short prices invalid: target={target_px} entry={entry_px} stop={stop_px}"
                )
        risk = abs(entry_px - stop_px)
        if risk <= 0:
            raise ValueError("stop must be different from entry")
        reward = abs(target_px - entry_px)
        break_even_stop = entry_px
        break_even_trigger = entry_px + risk if order.is_buy else entry_px - risk
        notional = order.sz * entry_px
        margin_usd = order.margin_usd or notional / max(float(order.leverage), 1.0)
        v1_plan = V1TradePlan(
            coin=order.coin,
            side=side,
            entry=entry_px,
            stop=stop_px,
            break_even_stop=break_even_stop,
            break_even_trigger=break_even_trigger,
            target=target_px,
            qty=order.sz,
            notional=notional,
            margin_usd=margin_usd,
            planned_leverage=float(order.leverage),
            initial_risk_usd=order.sz * risk,
            initial_risk_pct=0.0,
            reward_r=reward / risk,
            max_hold_candles=8,
            reason="V2 live order",
        )
        managed = build_managed_order_plan(v1_plan, seed=order.cloid or f"v2:{time.time_ns()}", entry_tif=order.entry_tif)
        for role, cloid in (
            ("entry_cloid", managed.entry.cloid),
            ("take_profit_cloid", managed.take_profit.cloid),
            ("stop_loss_cloid", managed.stop_loss.cloid),
        ):
            require_valid_cloid(cloid, role=role)
        result = self.v1.submit_initial_orders(managed, grouping="normalTpsl")
        errors = []
        statuses = result.get("response", {}).get("data", {}).get("statuses", []) if isinstance(result, dict) else []
        for status in statuses:
            if isinstance(status, dict) and status.get("error"):
                errors.append(str(status["error"]))
        if errors:
            raise RuntimeError("; ".join(errors))

        entry_status = statuses[0] if statuses else {}
        entry_filled = isinstance(entry_status, dict) and "filled" in entry_status
        if not entry_filled:
            for cloid in (managed.entry.cloid, managed.take_profit.cloid, managed.stop_loss.cloid):
                try:
                    self.v1.cancel_by_cloid(order.coin, cloid)
                except Exception as exc:
                    log.warning("Failed to cancel non-filled %s order %s: %s", order.coin, cloid, exc)
            raise RuntimeError(f"entry did not fill immediately; canceled resting order: {entry_status}")

        return {
            "result": result,
            "cloids": {
                "entry": managed.entry.cloid,
                "take_profit": managed.take_profit.cloid,
                "stop_loss": managed.stop_loss.cloid,
            },
        }

    def cancel_open_orders(self, coin: str) -> list[Any]:
        responses = []
        for order in self.open_orders():
            if order.get("coin") != coin:
                continue
            cloid = order.get("cloid")
            if cloid:
                responses.append(self.v1.cancel_by_cloid(coin, str(cloid)))
            elif order.get("oid") is not None:
                responses.append(self.exchange.cancel(coin, order["oid"]))
        return responses

    def cancel_by_cloid(self, coin: str, cloid: str) -> Any:
        require_valid_cloid(cloid)
        return self.v1.cancel_by_cloid(coin, cloid)

    def schedule_cancel(self, delay_seconds: int | None) -> Any:
        """Set or clear Hyperliquid's cancel-all dead-man switch."""
        if delay_seconds is None:
            return self.exchange.schedule_cancel(None)
        delay_seconds = max(int(delay_seconds), 5)
        return self.exchange.schedule_cancel(int((time.time() + delay_seconds) * 1000))

    def protective_order_state(
        self,
        coin: str,
        *,
        tp_cloid: str | None = None,
        sl_cloid: str | None = None,
    ) -> dict[str, Any]:
        orders = self.frontend_open_orders()
        coin_orders = [order for order in orders if str(order.get("coin") or "").upper() == coin.upper()]
        stops = []
        targets = []
        for order in coin_orders:
            if not _is_reduce_only(order):
                continue
            order_type = str(order.get("orderType") or order.get("order_type") or "").lower()
            cloid = str(order.get("cloid") or "")
            if (sl_cloid and cloid == sl_cloid) or "stop" in order_type:
                stops.append(order)
                continue
            if (tp_cloid and cloid == tp_cloid) or "take profit" in order_type or "take_profit" in order_type:
                targets.append(order)
        return {
            "ok": bool(stops) and bool(targets) and len(stops) == 1,
            "has_stop": bool(stops),
            "has_target": bool(targets),
            "stop_count": len(stops),
            "target_count": len(targets),
            "open_order_count": len(coin_orders),
            "stop_cloids": [order.get("cloid") for order in stops if order.get("cloid")],
            "target_cloids": [order.get("cloid") for order in targets if order.get("cloid")],
        }

    def _raise_on_order_errors(self, result: Any) -> None:
        errors = []
        statuses = result.get("response", {}).get("data", {}).get("statuses", []) if isinstance(result, dict) else []
        for status in statuses:
            if isinstance(status, dict) and status.get("error"):
                errors.append(str(status["error"]))
        if errors:
            raise RuntimeError("; ".join(errors))

    def update_stop_loss(self, coin: str, stop_px: float, sz: float, is_long: bool) -> Any:
        from hl_bb_bot.execution import _cloid, _wire_float
        from hyperliquid.utils.types import Cloid

        position = self.get_position(coin)
        if position is None:
            raise RuntimeError(f"cannot update stop for {coin}: no open exchange position")
        szi = float(position.get("szi") or 0.0)
        if is_long != (szi > 0):
            raise RuntimeError(f"cannot update stop for {coin}: side mismatch position_szi={szi}")
        expected = abs(float(sz))
        actual = abs(szi)
        if expected > 0 and abs(actual - expected) / expected > 0.05:
            raise RuntimeError(f"cannot update stop for {coin}: size mismatch expected={expected} actual={actual}")

        stop_px = _wire_float(self._round_price(coin, stop_px))
        qty = _wire_float(abs(sz))
        cloid = _cloid(f"v2-be:{time.time_ns()}", coin, "long" if is_long else "short", stop_px, qty, "stop_loss")
        require_valid_cloid(cloid, role="replacement_stop_cloid")
        result = self.exchange.order(
            coin,
            not is_long,
            qty,
            stop_px,
            {"trigger": {"triggerPx": stop_px, "isMarket": True, "tpsl": "sl"}},
            reduce_only=True,
            cloid=Cloid.from_str(cloid),
        )
        self._raise_on_order_errors(result)
        return {"result": result, "cloid": cloid}

    def close_position(self, coin: str) -> Any:
        return self.v1.market_close(coin)


def _is_reduce_only(order: dict[str, Any]) -> bool:
    value = order.get("reduceOnly")
    if value is None:
        value = order.get("reduce_only")
    if isinstance(value, str):
        return value.lower() == "true"
    return bool(value)
