"""TURBINE — pure rotation/direction/spread functions (port of turbine-producer.py v3.2.2).

Ported VERBATIM from senpi-skills/turbine/scripts/turbine-producer.py (choose_direction,
pick_rotation_asset, spread parsing) and turbine_config.py (normalize_coin_key). No I/O, no
MCP, no daemon — pure and unit-testable. `scan.py` fetches per-asset market data via
ctx.senpi_mcp and hands it to these helpers.

Both turbine legs (volume + runners) share THIS scoring module: the source ran the identical
volume-rotation alpha on both wallets and let the wallet boundary (DSL preset + sizing) select
behavior. So there is no per-leg branch here — only sizing/exit differ, and those live in the
recipe YAMLs, not in the signal logic.

Source thresholds preserved exactly:
  - Universe MAIN = [BTC, ETH, SOL, HYPE]; XYZ = [xyz:BRENTOIL, xyz:GOLD, xyz:SPX].
  - Direction = funding fade: LONG_CROWDED/LONG_HEAVY -> SHORT (funding_fade_short),
    SHORT_CROWDED/SHORT_HEAVY -> LONG (funding_fade_long), else random LONG/SHORT
    (alternate_neutral).
  - Rotation: probabilistic pool pick (random.random() < xyz_weight -> XYZ pool, else MAIN),
    then a deterministic rotation index inside the pool; skip held + 90s post-close cooldown;
    fall through to the OTHER pool if the chosen one is exhausted.
  - Spread gate: main <= spread_main_bps (3), xyz <= spread_xyz_bps (10). spread_bps from the
    top-of-book mid: ((ask - bid) / mid) * 10000.
"""

import random
import time


# ── Universe (shared by both wallets) ──

VOLUME_XYZ = ["xyz:BRENTOIL", "xyz:GOLD", "xyz:SPX"]
VOLUME_MAIN = ["BTC", "ETH", "SOL", "HYPE"]

POST_CLOSE_COOLDOWN_SECONDS = 90


def is_xyz(asset):
    return asset.startswith("xyz:")


def normalize_coin_key(coin):
    """Canonicalize a coin string for held_keys / cooldown sets.

    Port of turbine_config.normalize_coin_key (v3.2.2): preserve the xyz: prefix so that
    main:HYPE and xyz:HYPE are distinct slot occupiers. Lowercase the prefix, uppercase the
    symbol; bare symbols are uppercased.
    """
    if not coin:
        return ""
    s = coin.strip()
    if ":" in s:
        prefix, _, symbol = s.partition(":")
        return f"{prefix.lower()}:{symbol.upper()}"
    return s.upper()


def safe_float(v, d=0.0):
    try:
        return float(v)
    except (TypeError, ValueError):
        return d


# ── Direction: funding fade (verbatim from producer.choose_direction) ──

def choose_direction(regime, rng=random):
    """Funding fade. Crowded longs -> SHORT, crowded shorts -> LONG, flat -> coin flip.

    Returns (direction, thesis). `rng` is injectable for deterministic tests; the source used
    the module-level `random`.
    """
    r = (regime or "").upper()
    if r in ("LONG_CROWDED", "LONG_HEAVY"):
        return "SHORT", "funding_fade_short"
    if r in ("SHORT_CROWDED", "SHORT_HEAVY"):
        return "LONG", "funding_fade_long"
    return rng.choice(["LONG", "SHORT"]), "alternate_neutral"


# ── Spread parsing (verbatim from producer.query_asset_data) ──

def _lvl_px(lvl):
    if isinstance(lvl, dict):
        return safe_float(lvl.get("px", lvl.get("price", 0)))
    if isinstance(lvl, list) and lvl:
        return safe_float(lvl[0])
    return 0.0


def parse_asset_data(resp):
    """Extract bid/ask/mid/spread_bps + funding regime from a market_get_asset_data response.

    Returns a dict or None when the order book is missing / one-sided / non-positive (the
    source treated all of these as 'skip this asset'). Mirrors producer.query_asset_data
    exactly: top-of-book levels, mid = (bid+ask)/2, spread_bps = ((ask-bid)/mid)*10000.
    """
    if not resp or not isinstance(resp, dict):
        return None
    ad = resp.get("data", resp)
    if not isinstance(ad, dict):
        return None
    ob = ad.get("order_book") or ad.get("orderBook") or {}
    levels = ob.get("levels", [])
    if not isinstance(levels, list) or len(levels) < 2:
        return None
    bids, asks = levels[0], levels[1]
    if not bids or not asks:
        return None
    bid = _lvl_px(bids[0])
    ask = _lvl_px(asks[0])
    if bid <= 0 or ask <= 0:
        return None
    mid = (bid + ask) / 2.0
    spread_bps = ((ask - bid) / mid) * 10000 if mid > 0 else 999

    funding_regime = (
        ad.get("funding_regime") or ad.get("fundingRegime") or "UNKNOWN"
    ).upper()
    funding_annualized_pct = safe_float(
        ad.get("funding_annualized_pct") or ad.get("fundingAnnualizedPct") or 0
    )
    return {
        "bid": bid,
        "ask": ask,
        "mid": mid,
        "spread_bps": round(spread_bps, 3),
        "funding_regime": funding_regime,
        "funding_annualized_pct": funding_annualized_pct,
    }


def spread_ok(asset, spread_bps, spread_main_bps, spread_xyz_bps):
    """Spread gate: xyz assets get the wider xyz cap, everything else the main cap."""
    max_spread = spread_xyz_bps if is_xyz(asset) else spread_main_bps
    return spread_bps <= max_spread


# ── Rotation pick (verbatim from producer.pick_rotation_asset) ──

def pick_rotation_asset(rot_idx, xyz_weight, held_set, last_closed, now=None, rng=random):
    """Pick the next rotation asset.

    Probabilistic pool pick (random.random() < xyz_weight -> XYZ pool, else MAIN) + a
    deterministic rotation index inside the chosen pool. Skips assets already held and assets
    closed within POST_CLOSE_COOLDOWN_SECONDS (90s). Falls through to the OTHER pool if the
    chosen one is exhausted. Returns (asset|None, new_rot_idx).

    `now`/`rng` are injectable for deterministic tests; the source used time.time() / random.
    """
    if now is None:
        now = time.time()

    def _try_pool(pool, idx):
        n = len(pool)
        for _ in range(n):
            candidate = pool[idx % n]
            idx = (idx + 1) % n
            coin_key = normalize_coin_key(candidate)
            if coin_key in held_set:
                continue
            last = last_closed.get(coin_key, {})
            if last and (now - last.get("ts", 0)) < POST_CLOSE_COOLDOWN_SECONDS:
                continue
            return candidate, idx
        return None, idx

    use_xyz = rng.random() < xyz_weight
    pool = VOLUME_XYZ if use_xyz else VOLUME_MAIN
    asset, rot_idx = _try_pool(pool, rot_idx)
    if asset is not None:
        return asset, rot_idx

    other = VOLUME_MAIN if use_xyz else VOLUME_XYZ
    asset, rot_idx = _try_pool(other, rot_idx)
    return asset, rot_idx


def clamp_leverage(desired, venue_max):
    """Clamp desired leverage to the asset's Hyperliquid venue max.

    Turbine's source used a fixed 5x and relied on the runtime to clamp per-asset. We carry the
    same defensive clamp the spider port uses so a low-venue-cap asset never over-levers.
    """
    try:
        venue = int(venue_max)
    except (TypeError, ValueError):
        venue = desired
    if venue <= 0:
        venue = desired
    return max(1, min(int(desired), venue))
