# AUTO-GENERATED by scripts/generate_sbe_decoder.py from paradex_1_0.xml
# Schema ID=1 Version=0  Released 2026-03-31  —  DO NOT EDIT MANUALLY
#
# Channel name note: XML comments say orders.{account}/fills.{account} but
# the SDK subscribes with orders.{market}/fills.{market}; codec routes by market.
#
# BookEvent note: SBE produces bids/asks arrays; JSON channel produces
# inserts/updates/deletes — the field shapes differ.
#
# Field name divergence from JSON API:
#   SBE trade_id   ↔ JSON id
#   SBE order_type ↔ JSON type
#   SBE seq_no     ↔ JSON seq
# order_book prefix scan: if a user subscribes two depth levels for the same
# market simultaneously the prefix scan in ws_client may be ambiguous.
from __future__ import annotations

import struct

from pydantic import BaseModel, ConfigDict

_SCHEMA_ID = 1
_SCHEMA_VERSION = 0
_HEADER = struct.Struct("<HHHH")

INT64_MIN = -9223372036854775808


# ── Helpers ──────────────────────────────────────────────────────────────


def _ts(x: int) -> int:
    """Convert microseconds to milliseconds."""
    return x // 1000


def _f8(x: int) -> str:
    """Decode fixed-point int64 with exponent -8 to decimal string."""
    sign = "-" if x < 0 else ""
    x_abs = abs(x)
    integer_part = x_abs // 100_000_000
    frac_part = x_abs % 100_000_000
    return f"{sign}{integer_part}.{frac_part:08d}"


def _f8n(x: int) -> str | None:
    """Nullable _f8; INT64_MIN sentinel → None."""
    return None if x == INT64_MIN else _f8(x)


def _f12(x: int) -> str:
    """Decode fixed-point int64 with exponent -12 to decimal string."""
    sign = "-" if x < 0 else ""
    x_abs = abs(x)
    integer_part = x_abs // 1_000_000_000_000
    frac_part = x_abs % 1_000_000_000_000
    return f"{sign}{integer_part}.{frac_part:012d}"


def _f12n(x: int) -> str | None:
    """Nullable _f12; INT64_MIN sentinel → None."""
    return None if x == INT64_MIN else _f12(x)


_GROUP_HDR = struct.Struct("<HH")


def _addr(b: bytes) -> str:
    """Decode 32-byte big-endian AccountAddress to 0x-prefixed hex string."""
    val = int.from_bytes(b, "big")
    return "0x" + format(val, "x") if val else "0x0"


# ── OrderFlags bitset ──────────────────────────────────────────────────────

_ORDERFLAGS_FLAG_NAMES = {
    0: "REDUCE_ONLY",
    1: "STOP_CONDITION_BELOW_TRIGGER",
    2: "STOP_CONDITION_ABOVE_TRIGGER",
    3: "INTERACTIVE",
    4: "TARGET_STRATEGY_VWAP",
}


def _decode_orderflags(flags: int) -> list[str]:
    return [name for bit, name in _ORDERFLAGS_FLAG_NAMES.items() if flags & (1 << bit)]


# ── Enum maps ────────────────────────────────────────────────────────────

# PositionEvent.side uses LONG/SHORT semantics (BUY=LONG, SELL=SHORT per XML comment)
_ENUM_SIDE_LONG_SHORT = {1: "LONG", 2: "SHORT", 254: None}
_ENUM_SIDE = {1: "BUY", 2: "SELL", 254: None}
_ENUM_ORDERTYPE = {
    1: "LIMIT",
    2: "MARKET",
    3: "STOP_LIMIT",
    4: "STOP_MARKET",
    5: "TAKE_PROFIT_LIMIT",
    6: "TAKE_PROFIT_MARKET",
    7: "STOP_LOSS_LIMIT",
    8: "STOP_LOSS_MARKET",
    254: None,
}
_ENUM_ORDERSTATUS = {1: "NEW", 2: "UNTRIGGERED", 3: "OPEN", 4: "CLOSED", 254: None}
_ENUM_TIMEINFORCE = {1: "GTC", 2: "IOC", 3: "POST_ONLY", 4: "RPI", 254: None}
_ENUM_FILLTYPE = {1: "FILL", 2: "LIQUIDATION", 3: "TRANSFER", 4: "SETTLE_MARKET", 5: "RPI", 6: "BLOCK_TRADE", 254: None}
_ENUM_PKGTYPE = {0: "SNAPSHOT", 1: "DELTA"}
_ENUM_REDUCEONLY = {0: "FALSE", 1: "TRUE", 254: None}
_ENUM_LIQUIDITYSIDE = {1: "MAKER", 2: "TAKER", 254: None}
_ENUM_STPMODE = {0: "NONE", 1: "EXPIRE_MAKER", 2: "EXPIRE_TAKER", 3: "EXPIRE_BOTH", 254: None}
_ENUM_POSITIONSTATUS = {1: "OPEN", 2: "CLOSED", 3: "UNSPECIFIED", 254: None}
_ENUM_ACCOUNTSTATUS = {1: "ACTIVE", 2: "LIQUIDATION", 254: None}


# ── Pydantic models ──────────────────────────────────────────────────────


class TradeEventData(BaseModel):
    """Decoded TradeEvent (templateId=1)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    trade_id: int
    side: str | None
    price: str
    size: str
    created_at: int
    trade_type: str | None
    market: str


class BboEventData(BaseModel):
    """Decoded BboEvent (templateId=2)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    bid_price: str | None
    bid_size: str | None
    ask_price: str | None
    ask_size: str | None
    market: str


class BookEventData(BaseModel):
    """Decoded BookEvent (templateId=3)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    pkg_type: str | None
    best_bid_price: str | None
    best_bid_size: str | None
    best_ask_price: str | None
    best_ask_size: str | None
    relative_spread: str | None
    best_bid_api_price: str | None
    best_bid_api_size: str | None
    best_ask_api_price: str | None
    best_ask_api_size: str | None
    bids: list[list[str]]
    asks: list[list[str]]
    market: str


class MarketSummaryEventData(BaseModel):
    """Decoded MarketSummaryEvent (templateId=4)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    mark_price: str
    index_price: str
    last_price: str
    volume_usd24h: str
    open_interest: str
    funding_rate: str
    bid: str | None
    ask: str | None
    total_volume: str
    price_change_rate24h: str
    mark_iv: str | None
    bid_iv: str | None
    ask_iv: str | None
    last_iv: str | None
    delta: str | None
    gamma: str | None
    vega: str | None
    theta: str | None
    rho: str | None
    volga: str | None
    vanna: str | None
    future_funding_rate: str | None
    external_fair_price: str | None
    bid_size: str | None
    ask_size: str | None
    market: str


class FundingDataEventData(BaseModel):
    """Decoded FundingDataEvent (templateId=5)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    funding_rate: str
    funding_index: str
    created_at: int
    funding_premium: str
    funding_rate8h: str
    funding_period_hours: int
    market: str


class OrderEventData(BaseModel):
    """Decoded OrderEvent (templateId=20)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    status: str | None
    side: str | None
    order_type: str | None
    time_in_force: str | None
    price: str | None
    trigger_price: str | None
    size: str
    size_open: str
    avg_fill_price: str | None
    created_at: int
    updated_at: int
    account: str
    received_at: int
    published_at: int
    stp: str | None
    flags: list[str]
    order_id: str
    client_order_id: str
    market: str
    cancel_reason: str


class FillEventData(BaseModel):
    """Decoded FillEvent (templateId=21)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    fill_type: str | None
    side: str | None
    liquidity: str | None
    price: str
    size: str
    fee: str
    realized_pnl: str | None
    created_at: int
    account: str
    underlying_price: str
    realized_funding: str
    fill_id: str
    order_id: str
    client_order_id: str
    trade_id: str
    market: str


class PositionEventData(BaseModel):
    """Decoded PositionEvent (templateId=22)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    side: str | None
    size: str
    avg_entry_price: str
    unrealized_pnl: str
    realized_pnl: str
    mark_price: str
    liquidation_price: str | None
    leverage: str
    updated_at: int
    account: str
    avg_entry_price_usd: str
    cost: str
    cost_usd: str
    cached_funding_index: str
    unrealized_funding_pnl: str
    status: str | None
    market: str
    last_fill_id: str


class AccountEventData(BaseModel):
    """Decoded AccountEvent (templateId=23)."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    total_collateral: str
    free_collateral: str
    initial_margin_req: str
    maintenance_margin_req: str
    account_value: str
    unrealized_pnl: str
    updated_at: int
    account: str
    margin_cushion: str
    status: str | None
    settlement_asset: str


class HeartbeatEventData(BaseModel):
    """Decoded HeartbeatEvent (templateId=40). Channel: discarded."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int


class SubscribedEventData(BaseModel):
    """Decoded SubscribedEvent (templateId=41). Channel: discarded."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)
    timestamp: int
    seq_no: int
    status: int
    channel: str


# ── Decode functions ──────────────────────────────────────────────────────


def _read_str(buf: bytes, pos: int) -> tuple[str, int]:
    ln = buf[pos]
    return buf[pos + 1 : pos + 1 + ln].decode(), pos + 1 + ln


_TRADEEVENT_STRUCT = struct.Struct("<qqqBqqqB")


def _decode_1(payload: bytes, block_len: int) -> tuple[str, TradeEventData]:
    (
        ts_raw,
        seq_raw,
        trade_id_raw,
        side_raw,
        price_raw,
        size_raw,
        created_at_raw,
        trade_type_raw,
    ) = _TRADEEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    market, offset = _read_str(payload, offset)
    return "trades." + market, TradeEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        trade_id=trade_id_raw,
        side=_ENUM_SIDE.get(side_raw),
        price=_f8(price_raw),
        size=_f8(size_raw),
        created_at=_ts(created_at_raw),
        trade_type=_ENUM_FILLTYPE.get(trade_type_raw),
        market=market,
    )


_BBOEVENT_STRUCT = struct.Struct("<qqqqqq")


def _decode_2(payload: bytes, block_len: int) -> tuple[str, BboEventData]:
    ts_raw, seq_raw, bid_price_raw, bid_size_raw, ask_price_raw, ask_size_raw = _BBOEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    market, offset = _read_str(payload, offset)
    return "bbo." + market, BboEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        bid_price=_f8n(bid_price_raw),
        bid_size=_f8n(bid_size_raw),
        ask_price=_f8n(ask_price_raw),
        ask_size=_f8n(ask_size_raw),
        market=market,
    )


_BOOKEVENT_STRUCT = struct.Struct("<qqBqqqqqqqqq")


def _decode_3(payload: bytes, block_len: int) -> tuple[str, BookEventData]:
    (
        ts_raw,
        seq_raw,
        pkg_type_raw,
        best_bid_price_raw,
        best_bid_size_raw,
        best_ask_price_raw,
        best_ask_size_raw,
        relative_spread_raw,
        best_bid_api_price_raw,
        best_bid_api_size_raw,
        best_ask_api_price_raw,
        best_ask_api_size_raw,
    ) = _BOOKEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    # bids group
    _grp_blk, _num_bids = _GROUP_HDR.unpack_from(payload, offset)
    offset += 4
    bids: list[list[str]] = []
    _entry_bids = struct.Struct("<qq")
    for _ in range(_num_bids):
        _price_raw, _size_raw = _entry_bids.unpack_from(payload, offset)
        bids.append([_f8(_price_raw), _f8(_size_raw)])
        offset += _grp_blk
    # asks group
    _grp_blk, _num_asks = _GROUP_HDR.unpack_from(payload, offset)
    offset += 4
    asks: list[list[str]] = []
    _entry_asks = struct.Struct("<qq")
    for _ in range(_num_asks):
        _price_raw, _size_raw = _entry_asks.unpack_from(payload, offset)
        asks.append([_f8(_price_raw), _f8(_size_raw)])
        offset += _grp_blk
    market, offset = _read_str(payload, offset)
    return "order_book." + market, BookEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        pkg_type=_ENUM_PKGTYPE.get(pkg_type_raw),
        best_bid_price=_f8n(best_bid_price_raw),
        best_bid_size=_f8n(best_bid_size_raw),
        best_ask_price=_f8n(best_ask_price_raw),
        best_ask_size=_f8n(best_ask_size_raw),
        relative_spread=_f8n(relative_spread_raw),
        best_bid_api_price=_f8n(best_bid_api_price_raw),
        best_bid_api_size=_f8n(best_bid_api_size_raw),
        best_ask_api_price=_f8n(best_ask_api_price_raw),
        best_ask_api_size=_f8n(best_ask_api_size_raw),
        bids=bids,
        asks=asks,
        market=market,
    )


_MARKETSUMMARYEVENT_STRUCT = struct.Struct("<qqqqqqqqqqqqqqqqqqqqqqqqqqq")


def _decode_4(payload: bytes, block_len: int) -> tuple[str, MarketSummaryEventData]:
    (
        ts_raw,
        seq_raw,
        mark_price_raw,
        index_price_raw,
        last_price_raw,
        volume_usd24h_raw,
        open_interest_raw,
        funding_rate_raw,
        bid_raw,
        ask_raw,
        total_volume_raw,
        price_change_rate24h_raw,
        mark_iv_raw,
        bid_iv_raw,
        ask_iv_raw,
        last_iv_raw,
        delta_raw,
        gamma_raw,
        vega_raw,
        theta_raw,
        rho_raw,
        volga_raw,
        vanna_raw,
        future_funding_rate_raw,
        external_fair_price_raw,
        bid_size_raw,
        ask_size_raw,
    ) = _MARKETSUMMARYEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    market, offset = _read_str(payload, offset)
    return "markets_summary." + market, MarketSummaryEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        mark_price=_f8(mark_price_raw),
        index_price=_f8(index_price_raw),
        last_price=_f8(last_price_raw),
        volume_usd24h=_f8(volume_usd24h_raw),
        open_interest=_f8(open_interest_raw),
        funding_rate=_f8(funding_rate_raw),
        bid=_f8n(bid_raw),
        ask=_f8n(ask_raw),
        total_volume=_f8(total_volume_raw),
        price_change_rate24h=_f8(price_change_rate24h_raw),
        mark_iv=_f8n(mark_iv_raw),
        bid_iv=_f8n(bid_iv_raw),
        ask_iv=_f8n(ask_iv_raw),
        last_iv=_f8n(last_iv_raw),
        delta=_f8n(delta_raw),
        gamma=_f8n(gamma_raw),
        vega=_f8n(vega_raw),
        theta=_f8n(theta_raw),
        rho=_f8n(rho_raw),
        volga=_f8n(volga_raw),
        vanna=_f8n(vanna_raw),
        future_funding_rate=_f8n(future_funding_rate_raw),
        external_fair_price=_f8n(external_fair_price_raw),
        bid_size=_f8n(bid_size_raw),
        ask_size=_f8n(ask_size_raw),
        market=market,
    )


_FUNDINGDATAEVENT_STRUCT = struct.Struct("<qqqqqqqh")


def _decode_5(payload: bytes, block_len: int) -> tuple[str, FundingDataEventData]:
    (
        ts_raw,
        seq_raw,
        funding_rate_raw,
        funding_index_raw,
        created_at_raw,
        funding_premium_raw,
        funding_rate8h_raw,
        funding_period_hours_raw,
    ) = _FUNDINGDATAEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    market, offset = _read_str(payload, offset)
    return "funding_data." + market, FundingDataEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        funding_rate=_f8(funding_rate_raw),
        funding_index=_f8(funding_index_raw),
        created_at=_ts(created_at_raw),
        funding_premium=_f8(funding_premium_raw),
        funding_rate8h=_f8(funding_rate8h_raw),
        funding_period_hours=funding_period_hours_raw,
        market=market,
    )


_ORDEREVENT_STRUCT = struct.Struct("<qqBBBBqqqqqqq32sqqBB")


def _decode_20(payload: bytes, block_len: int) -> tuple[str, OrderEventData]:
    (
        ts_raw,
        seq_raw,
        status_raw,
        side_raw,
        order_type_raw,
        time_in_force_raw,
        price_raw,
        trigger_price_raw,
        size_raw,
        size_open_raw,
        avg_fill_price_raw,
        created_at_raw,
        updated_at_raw,
        account_raw,
        received_at_raw,
        published_at_raw,
        stp_raw,
        flags_raw,
    ) = _ORDEREVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    order_id, offset = _read_str(payload, offset)
    client_order_id, offset = _read_str(payload, offset)
    market, offset = _read_str(payload, offset)
    cancel_reason, offset = _read_str(payload, offset)
    return "orders." + market, OrderEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        status=_ENUM_ORDERSTATUS.get(status_raw),
        side=_ENUM_SIDE.get(side_raw),
        order_type=_ENUM_ORDERTYPE.get(order_type_raw),
        time_in_force=_ENUM_TIMEINFORCE.get(time_in_force_raw),
        price=_f8n(price_raw),
        trigger_price=_f8n(trigger_price_raw),
        size=_f8(size_raw),
        size_open=_f8(size_open_raw),
        avg_fill_price=_f8n(avg_fill_price_raw),
        created_at=_ts(created_at_raw),
        updated_at=_ts(updated_at_raw),
        account=_addr(account_raw),
        received_at=_ts(received_at_raw),
        published_at=_ts(published_at_raw),
        stp=_ENUM_STPMODE.get(stp_raw),
        flags=_decode_orderflags(flags_raw),
        order_id=order_id,
        client_order_id=client_order_id,
        market=market,
        cancel_reason=cancel_reason,
    )


_FILLEVENT_STRUCT = struct.Struct("<qqBBBqqqqq32sqq")


def _decode_21(payload: bytes, block_len: int) -> tuple[str, FillEventData]:
    (
        ts_raw,
        seq_raw,
        fill_type_raw,
        side_raw,
        liquidity_raw,
        price_raw,
        size_raw,
        fee_raw,
        realized_pnl_raw,
        created_at_raw,
        account_raw,
        underlying_price_raw,
        realized_funding_raw,
    ) = _FILLEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    fill_id, offset = _read_str(payload, offset)
    order_id, offset = _read_str(payload, offset)
    client_order_id, offset = _read_str(payload, offset)
    trade_id, offset = _read_str(payload, offset)
    market, offset = _read_str(payload, offset)
    return "fills." + market, FillEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        fill_type=_ENUM_FILLTYPE.get(fill_type_raw),
        side=_ENUM_SIDE.get(side_raw),
        liquidity=_ENUM_LIQUIDITYSIDE.get(liquidity_raw),
        price=_f8(price_raw),
        size=_f8(size_raw),
        fee=_f8(fee_raw),
        realized_pnl=_f8n(realized_pnl_raw),
        created_at=_ts(created_at_raw),
        account=_addr(account_raw),
        underlying_price=_f8(underlying_price_raw),
        realized_funding=_f8(realized_funding_raw),
        fill_id=fill_id,
        order_id=order_id,
        client_order_id=client_order_id,
        trade_id=trade_id,
        market=market,
    )


_POSITIONEVENT_STRUCT = struct.Struct("<qqBqqqqqqqq32sqqqqqB")


def _decode_22(payload: bytes, block_len: int) -> tuple[str, PositionEventData]:
    (
        ts_raw,
        seq_raw,
        side_raw,
        size_raw,
        avg_entry_price_raw,
        unrealized_pnl_raw,
        realized_pnl_raw,
        mark_price_raw,
        liquidation_price_raw,
        leverage_raw,
        updated_at_raw,
        account_raw,
        avg_entry_price_usd_raw,
        cost_raw,
        cost_usd_raw,
        cached_funding_index_raw,
        unrealized_funding_pnl_raw,
        status_raw,
    ) = _POSITIONEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    market, offset = _read_str(payload, offset)
    last_fill_id, offset = _read_str(payload, offset)
    return "positions", PositionEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        side=_ENUM_SIDE_LONG_SHORT.get(side_raw),
        size=_f8(size_raw),
        avg_entry_price=_f8(avg_entry_price_raw),
        unrealized_pnl=_f8(unrealized_pnl_raw),
        realized_pnl=_f8(realized_pnl_raw),
        mark_price=_f8(mark_price_raw),
        liquidation_price=_f8n(liquidation_price_raw),
        leverage=_f8(leverage_raw),
        updated_at=_ts(updated_at_raw),
        account=_addr(account_raw),
        avg_entry_price_usd=_f8(avg_entry_price_usd_raw),
        cost=_f8(cost_raw),
        cost_usd=_f8(cost_usd_raw),
        cached_funding_index=_f8(cached_funding_index_raw),
        unrealized_funding_pnl=_f8(unrealized_funding_pnl_raw),
        status=_ENUM_POSITIONSTATUS.get(status_raw),
        market=market,
        last_fill_id=last_fill_id,
    )


_ACCOUNTEVENT_STRUCT = struct.Struct("<qqqqqqqqq32sqB")


def _decode_23(payload: bytes, block_len: int) -> tuple[str, AccountEventData]:
    (
        ts_raw,
        seq_raw,
        total_collateral_raw,
        free_collateral_raw,
        initial_margin_req_raw,
        maintenance_margin_req_raw,
        account_value_raw,
        unrealized_pnl_raw,
        updated_at_raw,
        account_raw,
        margin_cushion_raw,
        status_raw,
    ) = _ACCOUNTEVENT_STRUCT.unpack_from(payload, 0)
    offset = block_len
    settlement_asset, offset = _read_str(payload, offset)
    return "account", AccountEventData(
        timestamp=_ts(ts_raw),
        seq_no=seq_raw,
        total_collateral=_f8(total_collateral_raw),
        free_collateral=_f8(free_collateral_raw),
        initial_margin_req=_f8(initial_margin_req_raw),
        maintenance_margin_req=_f8(maintenance_margin_req_raw),
        account_value=_f8(account_value_raw),
        unrealized_pnl=_f8(unrealized_pnl_raw),
        updated_at=_ts(updated_at_raw),
        account=_addr(account_raw),
        margin_cushion=_f8(margin_cushion_raw),
        status=_ENUM_ACCOUNTSTATUS.get(status_raw),
        settlement_asset=settlement_asset,
    )


_HEARTBEATEVENT_STRUCT = struct.Struct("<qq")


def _decode_40(payload: bytes, block_len: int) -> tuple[None, None]:
    """Discard HeartbeatEvent — no callback routing."""
    return None, None


_SUBSCRIBEDEVENT_STRUCT = struct.Struct("<qqB")


def _decode_41(payload: bytes, block_len: int) -> tuple[None, None]:
    """Discard SubscribedEvent — no callback routing."""
    return None, None


# ── Dispatcher ────────────────────────────────────────────────────────────

_DECODERS: dict[int, object] = {
    1: _decode_1,
    2: _decode_2,
    3: _decode_3,
    4: _decode_4,
    5: _decode_5,
    20: _decode_20,
    21: _decode_21,
    22: _decode_22,
    23: _decode_23,
    40: _decode_40,
    41: _decode_41,
}


class SbeDecodeError(Exception):
    """Raised when an SBE binary frame cannot be decoded."""


def decode_frame(data: bytes) -> tuple[str | None, BaseModel | None]:
    """Decode a binary SBE WebSocket frame.

    Returns:
        (channel_name, model) — channel is None for heartbeat/subscribed ack.

    Raises:
        SbeDecodeError: on malformed or unsupported frames.
    """
    if len(data) < 8:
        raise SbeDecodeError(f"Frame too short: {len(data)} bytes")
    block_len, tmpl_id, schema_id, _ = _HEADER.unpack_from(data, 0)
    if schema_id != _SCHEMA_ID:
        raise SbeDecodeError(f"Unsupported schemaId {schema_id}, expected {_SCHEMA_ID}")
    dec = _DECODERS.get(tmpl_id)
    if dec is None:
        raise SbeDecodeError(f"Unknown templateId {tmpl_id}")
    return dec(data[8:], block_len)  # type: ignore[operator]
