"""RAPTOR — pure whale copy-follow functions (port of raptor-producer.py v4.0.1).

Ported VERBATIM from senpi-skills/raptor/scripts/raptor-producer.py (the
parsing + gating + additive scoring helpers) and raptor_config.py. No I/O, no
MCP, no daemon — pure and unit-testable. `scan.py` fetches trader/market data
via ctx.senpi_mcp and hands it to these helpers.

DOC-vs-CODE: the SKILL/README docs are STALE (they say trader gate $2M /
concentration 0.40). The CODE (config v3.1 + producer constants) is the source
of truth and is what is ported here:
  - trader delta gate (minDeltaPnl)        = $500,000
  - position delta gate (minPositionPnl)   = $100,000
  - concentration gate (minConcentration)  = 0.35
  - SM gate                                = minSmPct 2.0 / minSmTraders 10
  - whale entry-discipline                 = skip if price ran > 5% from entry
  - score floor (minScore)                 = 6
  - leverage tiers                         = >=10 -> 10x, >=8 -> 8x, >=6 -> 7x
  - tier2 / tier3 trader thresholds        = $1.5M / $3.0M
  - margin pct                             = 0.25 base, 0.35 if score >= 10

Source thresholds preserved exactly — see the scoring tier table in
score_position() (all additive, max ~16).
"""

# ── Constants (verbatim from producer + config v3.1) ──

XYZ_BANNED = True
MAX_PRICE_RUN_PCT_FROM_WHALE_ENTRY = 5.0   # v3.3 tightened from 20% -> 5%

# Trader-pool / position gates (config.hotStreak)
MIN_TRADER_DELTA_PNL = 500_000
MIN_POSITION_PNL = 100_000
MIN_CONCENTRATION = 0.35
TIER2_TRADER_THRESHOLD = 1_500_000
TIER3_TRADER_THRESHOLD = 3_000_000

# Smart-money alignment (config.smAlignment)
MIN_SM_PCT = 2.0
MIN_SM_TRADERS = 10
REQUIRE_DIRECTION_MATCH = True

# Entry (config.entry)
MIN_SCORE = 6
MARGIN_PCT_BASE = 0.25
MARGIN_PCT_HIGH_CONV = 0.35

# Leverage (config.leverage)
DEFAULT_LEVERAGE = 7
LEVERAGE_TIERS = [
    {"minScore": 10, "leverage": 10},
    {"minScore": 8, "leverage": 8},
    {"minScore": 6, "leverage": 7},
]

# Dedup (config.dedupe)
EVENT_DEDUPE_HOURS = 4

# Score normalization divisor used when posting the [0,1] score on the wire.
SCORE_NORMALIZATION_DIVISOR = 16.0


def safe_float(val, default=0.0):
    try:
        return float(val)
    except (TypeError, ValueError):
        return default


def _extract_list(raw, *candidate_paths):
    """Walk candidate dotted paths and return the first list found.

    Verbatim from producer._extract_list — the prod responses nest the actual
    list under varying keys across tools, so we try each path in order.
    """
    if raw is None:
        return []
    for path in candidate_paths:
        cur = raw
        for k in path:
            if isinstance(cur, dict) and k in cur:
                cur = cur[k]
            else:
                cur = None
                break
        if isinstance(cur, list):
            return cur
    if isinstance(raw, list):
        return raw
    if isinstance(raw, dict) and isinstance(raw.get("data"), list):
        return raw["data"]
    return []


# ── Trader-pool quality filter (verbatim from fetch_quality_hot_traders) ──

def parse_quality_traders(raw, min_delta_usd=MIN_TRADER_DELTA_PNL):
    """Filter a discovery_get_top_traders response to currently-winning quality
    traders: ELITE/RELIABLE consistency, unrealized delta-pnl >= min_delta_usd.

    Returns a list of trader dicts sorted by unrealized_pnl desc.
    """
    raw_list = _extract_list(raw, ("data", "traders"), ("traders",))
    quality = []
    for t in raw_list:
        if not isinstance(t, dict):
            continue
        addr = str(t.get("address", "")).lower()
        if not addr:
            continue
        unrealized = safe_float(t.get("unRealizedProfitAndLoss", 0))
        realized = safe_float(t.get("realizedProfitAndLoss", 0))
        total_pnl = safe_float(t.get("profitAndLoss", unrealized + realized))
        tcs_label = str(t.get("tcsLabel", "")).upper()
        if tcs_label not in ("ELITE", "RELIABLE"):
            continue
        if unrealized < min_delta_usd:
            continue
        quality.append({
            "address": addr,
            "unrealized_pnl": unrealized,
            "realized_pnl": realized,
            "total_pnl": total_pnl,
            "tcs_label": tcs_label,
            "tcs_value": safe_float(t.get("tcsValue", 0)),
            "roi": safe_float(t.get("returnOnInvestment", 0)),
        })
    quality.sort(key=lambda x: x["unrealized_pnl"], reverse=True)
    return quality


def parse_trader_positions(raw):
    """v3.4 nested-dict parser fix preserved — try each known position path."""
    return _extract_list(
        raw,
        ("data", "positions", "positions"),   # nested-dict (actual prod shape)
        ("data", "top_positions", "positions"),
        ("data", "positions"),
        ("data", "top_positions"),
        ("positions",),
        ("top_positions",),
    )


def parse_sm_map(raw):
    """Build {TOKEN: {direction, pct, traders, price_chg_4h, price_chg_1h,
    contrib_15m, contrib_1h}} from a leaderboard_get_markets response.

    xyz-dex markets are dropped (XYZ_BANNED). Verbatim from fetch_sm_map.
    """
    markets = []
    if isinstance(raw, dict):
        data = raw.get("data", raw)
        if isinstance(data, dict):
            markets = data.get("markets", [])
            if isinstance(markets, dict):
                markets = markets.get("markets", [])
        elif isinstance(data, list):
            markets = data
    elif isinstance(raw, list):
        markets = raw
    sm_map = {}
    for m in markets:
        if not isinstance(m, dict):
            continue
        token = str(m.get("token", "")).upper()
        dex = str(m.get("dex", "")).lower()
        if XYZ_BANNED and dex == "xyz":
            continue
        if not token:
            continue
        sm_map[token] = {
            "direction": str(m.get("direction", "")).upper(),
            "pct": safe_float(m.get("pct_of_top_traders_gain", 0)),
            "traders": int(m.get("trader_count", 0)),
            "price_chg_4h": safe_float(m.get("token_price_change_pct_4h", 0)),
            "price_chg_1h": safe_float(m.get("token_price_change_pct_1h",
                                             m.get("price_change_1h", 0))),
            "contrib_15m": safe_float(m.get("contribution_pct_change_15m", 0)),
            "contrib_1h": safe_float(m.get("contribution_pct_change_1h", 0)),
        }
    return sm_map


# ── Strongest-position selection + concentration (verbatim from build_signal) ──

def pick_strongest_position(positions):
    """Pick the position with the largest |delta_pnl|, and compute concentration.

    Returns (best_pos|None, best_abs_pnl, total_abs_pnl, concentration). xyz
    positions are skipped (XYZ_BANNED). best_pos carries asset/direction/
    delta_pnl/whale_entry_px. Verbatim from the build_signal position loop.
    """
    best_pos = None
    best_abs_pnl = 0.0
    total_abs_pnl = 0.0
    for pos in positions:
        if not isinstance(pos, dict):
            continue
        asset = str(
            pos.get("coin", pos.get("market", pos.get("asset", pos.get("symbol", ""))))
        ).upper()
        if not asset:
            continue
        if XYZ_BANNED and asset.lower().startswith("xyz:"):
            continue
        delta_pnl = safe_float(
            pos.get("delta_pnl",
                    pos.get("deltaPnl",
                            pos.get("unrealizedPnl",
                                    pos.get("unrealized_pnl",
                                            pos.get("pnl", 0)))))
        )
        direction = str(
            pos.get("direction",
                    pos.get("side",
                            "LONG" if delta_pnl >= 0 else "SHORT"))
        ).upper()
        if direction not in ("LONG", "SHORT"):
            continue
        whale_entry_px = safe_float(
            pos.get("entryPx",
                    pos.get("entry_px",
                            pos.get("entryPrice",
                                    pos.get("entry_price",
                                            pos.get("avgEntryPx",
                                                    pos.get("avg_entry_px", 0))))))
        )
        abs_pnl = abs(delta_pnl)
        total_abs_pnl += abs_pnl
        if abs_pnl > best_abs_pnl:
            best_abs_pnl = abs_pnl
            best_pos = {
                "asset": asset,
                "direction": direction,
                "delta_pnl": delta_pnl,
                "whale_entry_px": whale_entry_px,
            }
    concentration = (best_abs_pnl / total_abs_pnl) if total_abs_pnl > 0 else 0
    return best_pos, best_abs_pnl, total_abs_pnl, concentration


def sm_alignment_ok(sm, direction,
                    min_sm_pct=MIN_SM_PCT, min_sm_traders=MIN_SM_TRADERS,
                    require_direction_match=REQUIRE_DIRECTION_MATCH):
    """Smart-money alignment gate (verbatim from build_signal's SM checks).

    `sm` is the per-asset SM record (or None). Returns True only if SM data
    exists, direction matches (when required), pct >= min_sm_pct, and
    traders >= min_sm_traders.
    """
    if not sm:
        return False
    if require_direction_match and sm.get("direction") != direction:
        return False
    if safe_float(sm.get("pct", 0)) < min_sm_pct:
        return False
    if int(sm.get("traders", 0)) < min_sm_traders:
        return False
    return True


def entry_discipline_ok(direction, whale_entry_px, current_px,
                        max_run_pct=MAX_PRICE_RUN_PCT_FROM_WHALE_ENTRY):
    """v3.3 whale entry-discipline gate: don't buy the whale's top.

    Returns False (skip) when the price has already run more than `max_run_pct`
    in the whale's favored direction beyond the whale's entry. Only enforced
    when both prices are positive (matches the source: no price -> don't gate).
    Verbatim from build_signal's run_pct check.
    """
    if whale_entry_px <= 0 or current_px <= 0:
        return True
    if direction == "LONG":
        run_pct = ((current_px - whale_entry_px) / whale_entry_px) * 100
    else:
        run_pct = ((whale_entry_px - current_px) / whale_entry_px) * 100
    return run_pct <= max_run_pct


def get_leverage_for_score(score, tiers=LEVERAGE_TIERS, default_leverage=DEFAULT_LEVERAGE):
    """Conviction-tier leverage: highest matching minScore tier wins.

    Verbatim from producer.get_leverage_for_score.
    """
    for tier in sorted(tiers, key=lambda t: t.get("minScore", 0), reverse=True):
        if score >= tier.get("minScore", 0):
            return tier.get("leverage", default_leverage)
    return default_leverage


def margin_pct_for_score(score, base=MARGIN_PCT_BASE, high_conv=MARGIN_PCT_HIGH_CONV):
    """Conviction-tier sizing: high-conviction (score >= 10) sizes up.

    Verbatim from producer.main's margin_pct selection.
    """
    return high_conv if score >= 10 else base


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

    Port of producer.get_safe_leverage's min(requested, venue_max) with a
    safe-default fallback (the source defaulted the venue cap to 20 when the
    limits payload lacked a usable value; here a missing/<=0 venue_max means we
    keep the desired leverage and let the runtime clamp further if needed).
    """
    try:
        venue = int(float(venue_max))
    except (TypeError, ValueError):
        venue = desired
    if venue <= 0:
        venue = desired
    return max(1, min(int(desired), venue))


# ── Additive scoring (verbatim tier-for-tier from build_signal) ──

def score_position(trader, best_pos, concentration, sm, current_px):
    """Compute the additive copy-follow score (max ~16) for a candidate.

    ALL tiers ported VERBATIM from build_signal's scoring block:
      - TCS:        ELITE +3, RELIABLE +2, else -> None (reject)
      - trader $:   >= $3M +3, >= $1.5M +2, else +1
      - ROI:        >= 50% +1
      - conc:       >= 0.70 +2, >= 0.55 +1
      - SM pct:     >= 8 +2, >= 4 +1
      - 4h/1h:      directional confirm +2 / +1, opposing -1
      - 15m:        contrib > 0.5 +1, <= 0 -1
      - edge bonus: better-than-whale +2 (>=5%) / +1 (>=2%)

    Returns (score, reasons) or None when TCS is neither ELITE nor RELIABLE.
    `current_px` may be 0 (entry-discipline bonus only fires with a real price).
    """
    direction = best_pos["direction"]
    whale_entry_px = safe_float(best_pos.get("whale_entry_px", 0))
    score = 0
    reasons = []

    tcs = trader["tcs_label"]
    if tcs == "ELITE":
        score += 3
        reasons.append(f"ELITE_tcs{trader['tcs_value']:.0f}")
    elif tcs == "RELIABLE":
        score += 2
        reasons.append(f"RELIABLE_tcs{trader['tcs_value']:.0f}")
    else:
        return None

    trader_delta = trader["unrealized_pnl"]
    if trader_delta >= TIER3_TRADER_THRESHOLD:
        score += 3
        reasons.append(f"TIER3_${trader_delta/1e6:.1f}M")
    elif trader_delta >= TIER2_TRADER_THRESHOLD:
        score += 2
        reasons.append(f"TIER2_${trader_delta/1e6:.1f}M")
    else:
        score += 1
        reasons.append(f"TIER1_${trader_delta/1e6:.1f}M")

    if trader["roi"] >= 50:
        score += 1
        reasons.append(f"ROI_{trader['roi']:.0f}%")

    if concentration >= 0.70:
        score += 2
        reasons.append(f"HIGH_CONV_{concentration:.0%}")
    elif concentration >= 0.55:
        score += 1
        reasons.append(f"CONC_{concentration:.0%}")

    if sm["pct"] >= 8:
        score += 2
        reasons.append(f"SM_STRONG_{sm['pct']:.1f}%")
    elif sm["pct"] >= 4:
        score += 1
        reasons.append(f"SM_ALIGNED_{sm['pct']:.1f}%")

    p4h = sm["price_chg_4h"]
    p1h = sm["price_chg_1h"]
    if direction == "LONG":
        if p4h > 0.5 and p1h > 0.2:
            score += 2
            reasons.append(f"4H+1H_CONFIRMS_+{p4h:.1f}/+{p1h:.1f}%")
        elif p4h > 0.5:
            score += 1
            reasons.append(f"4H_CONFIRMS_+{p4h:.1f}%")
        elif p4h < -2:
            score -= 1
            reasons.append(f"4H_OPPOSING_{p4h:.1f}%")
    else:
        if p4h < -0.5 and p1h < -0.2:
            score += 2
            reasons.append(f"4H+1H_CONFIRMS_{p4h:.1f}/{p1h:.1f}%")
        elif p4h < -0.5:
            score += 1
            reasons.append(f"4H_CONFIRMS_{p4h:.1f}%")
        elif p4h > 2:
            score -= 1
            reasons.append(f"4H_OPPOSING_+{p4h:.1f}%")

    c15m = sm.get("contrib_15m", 0)
    if c15m > 0.5:
        score += 1
        reasons.append(f"15M_SPIKE_+{c15m:.2f}")
    elif c15m <= 0:
        score -= 1
        reasons.append(f"15M_STALE_{c15m:.2f}")

    # v3.2 entry-discipline BONUS (better-than-whale entry)
    if whale_entry_px > 0 and current_px > 0:
        if direction == "LONG":
            edge_pct = ((whale_entry_px - current_px) / whale_entry_px) * 100
        else:
            edge_pct = ((current_px - whale_entry_px) / whale_entry_px) * 100
        if edge_pct >= 5:
            score += 2
            reasons.append(f"BETTER_THAN_WHALE_+{edge_pct:.1f}%")
        elif edge_pct >= 2:
            score += 1
            reasons.append(f"EDGE_VS_WHALE_+{edge_pct:.1f}%")

    return score, reasons


# ── Per-(trader, asset) event dedup (verbatim from producer dedup helpers) ──

def dedup_key(trader_id, asset):
    """Stable per-(trader, asset) dedup key. Verbatim from the producer:
    first 10 chars of the trader address (lowercased) + the asset symbol."""
    return f"{trader_id[:10].lower()}:{asset}"


def is_event_seen(seen, trader_id, asset, dedupe_hours=EVENT_DEDUPE_HOURS, now=None):
    """True if (trader, asset) was signaled within `dedupe_hours`. `seen` is a
    {key: epoch_seconds} map. Verbatim from producer.is_event_seen."""
    import time as _time
    if now is None:
        now = _time.time()
    ts = seen.get(dedup_key(trader_id, asset), 0)
    if ts <= 0:
        return False
    return (now - ts) < (dedupe_hours * 3600)


def prune_seen_events(seen, dedupe_hours=EVENT_DEDUPE_HOURS, now=None):
    """Drop dedup entries older than the window (bounds the map). Verbatim from
    producer.save_seen_events's cutoff filter."""
    import time as _time
    if now is None:
        now = _time.time()
    cutoff = now - (dedupe_hours * 3600)
    return {k: v for k, v in seen.items() if v > cutoff}
