from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any


@dataclass(frozen=True)
class LocalOrderRecord:
    coin: str
    client_order_id: str
    order_role: str
    size: Decimal | None = None


@dataclass(frozen=True)
class ReconcilerSnapshot:
    positions: dict[str, dict[str, Any]]
    open_orders: list[dict[str, Any]]
    local_orders: list[LocalOrderRecord]


@dataclass(frozen=True)
class ReconciliationResult:
    new_entries_allowed: bool
    stops_missing_count: int
    alerts: tuple[str, ...]
    block_new_entries: bool = False
    severity: str = "OK"
    recommended_actions: tuple[dict[str, Any], ...] = field(default_factory=tuple)
    stops_by_position: dict[str, list[dict[str, Any]]] = field(default_factory=dict)
    exposure_mismatch: tuple[str, ...] = ()


def _position_size(position: dict[str, Any]) -> Decimal:
    for key in ("szi", "contracts", "size"):
        if key in position:
            return Decimal(str(position.get(key) or "0"))
    nested = position.get("position")
    if isinstance(nested, dict):
        return _position_size(nested)
    return Decimal("0")


def _order_size(order: dict[str, Any]) -> Decimal:
    for key in ("sz", "size", "origSz"):
        if key in order:
            return abs(Decimal(str(order.get(key) or "0")))
    return Decimal("0")


def _position_side(position: dict[str, Any]) -> str:
    explicit = str(position.get("side", "")).lower()
    if explicit in {"long", "short"}:
        return explicit
    return "long" if _position_size(position) > 0 else "short"


def _order_side(order: dict[str, Any]) -> str:
    side = str(order.get("side", "")).lower()
    if side in {"buy", "b", "bid"}:
        return "buy"
    if side in {"sell", "s", "a", "ask"}:
        return "sell"
    if order.get("isBuy") is True or order.get("is_buy") is True:
        return "buy"
    if order.get("isBuy") is False or order.get("is_buy") is False:
        return "sell"
    return side


def _is_reduce_only_trigger(order: dict[str, Any]) -> bool:
    reduce_only = bool(order.get("reduceOnly") or order.get("reduce_only"))
    order_type = str(order.get("orderType") or order.get("order_type") or order.get("type") or "").lower()
    # Hyperliquid testnet open-order payloads for protective trigger stops may
    # expose only reduceOnly=true plus side/size/limitPx, without a literal
    # orderType/trigger/tpsl marker. Treat reduce-only open orders as protective
    # for the missing-stop gate; size/direction checks below still validate them.
    return reduce_only and (not order_type or "trigger" in order_type or "stop" in order_type or "take" in order_type)


def _stop_orders_for_coin(coin: str, open_orders: list[dict[str, Any]]) -> list[dict[str, Any]]:
    return [order for order in open_orders if str(order.get("coin", "")).upper() == coin.upper() and _is_reduce_only_trigger(order)]


def _has_reduce_only_stop(coin: str, open_orders: list[dict[str, Any]]) -> bool:
    return bool(_stop_orders_for_coin(coin, open_orders))


def reconcile_hyperliquid_state(
    snapshot: ReconcilerSnapshot,
    *,
    max_position_notional_usd: Decimal | int | float | None = None,
    mids: dict[str, Decimal | int | float] | None = None,
) -> ReconciliationResult:
    alerts: list[str] = []
    recommended: list[dict[str, Any]] = []
    exposure: list[str] = []
    stops_by_position: dict[str, list[dict[str, Any]]] = {}
    missing = 0
    local_entry_coins = {record.coin.upper() for record in snapshot.local_orders if record.order_role == "entry"}
    exchange_position_coins: set[str] = set()

    for coin, position in snapshot.positions.items():
        coin = coin.upper()
        size = _position_size(position)
        if size == 0:
            continue
        exchange_position_coins.add(coin)
        stops = _stop_orders_for_coin(coin, snapshot.open_orders)
        stops_by_position[coin] = stops
        if not stops:
            missing += 1
            alerts.append(f"position_without_stop:{coin}")
        if coin not in local_entry_coins:
            alerts.append(f"exchange_position_without_local_journal:{coin}")
            exposure.append(coin)
        position_abs = abs(size)
        pos_side = _position_side(position)
        expected_stop_side = "sell" if pos_side == "long" else "buy"
        for stop in stops:
            stop_size = _order_size(stop)
            if stop_size != 0 and stop_size != position_abs:
                alerts.append(f"stop_wrong_size:{coin}")
            if _order_side(stop) and _order_side(stop) != expected_stop_side:
                alerts.append(f"stop_wrong_direction:{coin}")
        if max_position_notional_usd is not None and mids and coin in mids:
            notional = position_abs * Decimal(str(mids[coin]))
            if notional > Decimal(str(max_position_notional_usd)):
                alerts.append(f"position_exposure_exceeded:{coin}")
                exposure.append(coin)

    for coin in sorted(local_entry_coins - exchange_position_coins):
        alerts.append(f"local_journal_position_without_exchange_position:{coin}")

    for order in snapshot.open_orders:
        reduce_only = bool(order.get("reduceOnly") or order.get("reduce_only"))
        if not reduce_only and str(order.get("orderType") or order.get("order_type") or "").lower() in {"limit", "market", "entry"}:
            if not bool(order.get("deadmanProtected") or order.get("deadman_protected")):
                alerts.append(f"entry_order_without_deadman:{str(order.get('coin', 'UNKNOWN')).upper()}")

    critical = any(alert.startswith(("position_without_stop", "stop_wrong", "position_exposure_exceeded", "exchange_position_without_local_journal")) for alert in alerts)
    if critical:
        recommended.append({"action": "prepare_telegram_alert", "severity": "CRITICAL", "message": ";".join(alerts)})
    if missing:
        recommended.append({"action": "block_new_entries", "reason": "stops_missing"})
    return ReconciliationResult(
        new_entries_allowed=not critical,
        stops_missing_count=missing,
        alerts=tuple(alerts),
        block_new_entries=critical,
        severity="CRITICAL" if critical else ("WARNING" if alerts else "OK"),
        recommended_actions=tuple(recommended),
        stops_by_position=stops_by_position,
        exposure_mismatch=tuple(sorted(set(exposure))),
    )
