"""Measure the one lane the sports-edge report left open: liquidity rewards.

Five of six paths were closed by fees, structure, or jurisdiction. The only
activity with a positive expected sign was being the MAKER who collects the
fee and the liquidity reward, not the taker who is right. This script measures
what a snapshot can measure about that lane, and is explicit about the one
thing it cannot.

WHAT THIS MEASURES (reproducible from the live Gamma + CLOB APIs):
- How many markets carry a reward pool, by category, and the pool size ($/day).
- rewardsMinSize (min qualifying quote) and rewardsMaxSpread (how tight you must
  quote to earn), versus the market's current spread.
- The minimum capital to post a qualifying two-sided quote.
- An UPPER BOUND on a small maker's daily reward: pool x (your size / total
  qualifying size), where total is estimated from resting book depth inside the
  reward band. Real competition only dilutes this further.

WHAT IT CANNOT MEASURE (stated, not hidden):
- Adverse selection. A resting maker is filled precisely when the price is about
  to move against them. Whether the reward exceeds that bleed cannot be read
  from a snapshot — it needs paper-quoting with honest fills. That is exactly
  what polybot-v2's paper maker-quoting engine is for.

Read-only, stdlib only, never authenticates, never orders.

    python research/polymarket_rewards_check.py
    python research/polymarket_rewards_check.py --account 1500 --json out.json
"""

from __future__ import annotations

import argparse
import json
import statistics
import sys
import urllib.error
import urllib.request
from typing import Any

GAMMA = "https://gamma-api.polymarket.com"
CLOB = "https://clob.polymarket.com"
UA = {"User-Agent": "nulledge-research/1.0 (read-only rewards measurement)"}

# Maker rebate share of the taker fee, by fee category (verified 2026-07-22).
# Sports pays the lowest rebate on the board; see polymarket_sports_fee_check.py.
REBATE_SHARE = {
    "sports_fees_v2": 0.15,
    "economics_fees": 0.25,
    "politics_fees": 0.25,
    "finance_prices_fees": 0.25,
    "crypto_fees_v2": 0.20,
    "culture_fees": 0.25,
}
MIN_DAILY_PAYOUT = 1.0  # Polymarket pays nothing below $1/day of accrued reward.


def fetch(url: str, timeout: int = 30) -> Any:
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.load(resp)


def daily_pool(market: dict) -> float:
    """Sum the active clobRewards daily rates for a market, in dollars/day."""
    total = 0.0
    for r in market.get("clobRewards") or []:
        rate = r.get("rewardsDailyRate")
        if isinstance(rate, (int, float)):
            total += float(rate)
    return total


def token_ids(market: dict) -> list[str]:
    raw = market.get("clobTokenIds")
    if isinstance(raw, str):
        try:
            raw = json.loads(raw)
        except json.JSONDecodeError:
            return []
    return raw if isinstance(raw, list) else []


def best_bid_ask(token_id: str) -> tuple[float | None, float | None]:
    try:
        b = fetch(f"{CLOB}/book?token_id={token_id}", timeout=15)
    except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError):
        return None, None
    bids, asks = b.get("bids") or [], b.get("asks") or []
    # CLOB: bids ascending, asks descending — best is the LAST element.
    bid = float(bids[-1]["price"]) if bids else None
    ask = float(asks[-1]["price"]) if asks else None
    return bid, ask


def qualifying_depth(token_id: str, mid: float, max_spread_cents: float) -> float:
    """Resting size (shares) within the reward band [mid +/- max_spread] on both
    sides. This is the pool a small maker's own size competes against."""
    try:
        b = fetch(f"{CLOB}/book?token_id={token_id}", timeout=15)
    except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError):
        return 0.0
    band = max_spread_cents / 100.0
    total = 0.0
    for side in ("bids", "asks"):
        for lvl in b.get(side) or []:
            price = float(lvl["price"])
            if abs(price - mid) <= band:
                total += float(lvl["size"])
    return total


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--account", type=float, default=1500.0, help="account size USD")
    ap.add_argument("--limit", type=int, default=100)
    ap.add_argument("--books", type=int, default=20, help="reward books to depth-probe")
    ap.add_argument("--json", type=str, default=None)
    args = ap.parse_args()

    print("Polymarket liquidity-rewards lane — snapshot measurement")
    print("=" * 68)

    markets = fetch(
        f"{GAMMA}/markets?limit={args.limit}&closed=false"
        f"&order=volume24hr&ascending=false"
    )
    reward_markets = [m for m in markets if daily_pool(m) > 0 and m.get("rewardsMinSize")]

    by_cat: dict[str, list[float]] = {}
    for m in reward_markets:
        by_cat.setdefault(m.get("feeType") or "(none)", []).append(daily_pool(m))

    print(f"\nSampled {len(markets)} live markets by 24h volume.")
    print(f"With an active reward pool: {len(reward_markets)}\n")
    print(f"{'category':<22} {'mkts':>5} {'pool $/day (median)':>20} {'total $/day':>12}")
    print("-" * 64)
    for cat, pools in sorted(by_cat.items(), key=lambda kv: -sum(kv[1])):
        print(f"{cat:<22} {len(pools):>5} {statistics.median(pools):>20.0f} {sum(pools):>12.0f}")

    sports = [m for m in reward_markets if m.get("feeType") == "sports_fees_v2"]
    print(f"\nSports reward markets: {len(sports)} of "
          f"{sum(1 for m in markets if m.get('feeType') == 'sports_fees_v2')} sports sampled")

    # Capital gate: can the account even post a qualifying two-sided quote?
    # Probe the highest-pool reward markets across ALL categories — the maker
    # question is not sports-specific, and the fattest pools set the ceiling.
    print(f"\nAccount ${args.account:.0f} — can it post a qualifying two-sided quote?")
    print(f"{'market':22}{'minSize':>9}{'maxSpr':>8}{'$ to qualify':>14}{'':>6}")
    print("-" * 64)
    probe = sorted(reward_markets, key=daily_pool, reverse=True)[: args.books]
    rows = []
    excluded_small = 0
    for m in probe:
        ids = token_ids(m)
        if len(ids) != 2:
            continue
        bid, ask = best_bid_ask(ids[0])
        if bid is None or ask is None:
            continue
        mid = (bid + ask) / 2.0
        min_size = float(m.get("rewardsMinSize") or 0)
        max_spread = float(m.get("rewardsMaxSpread") or 0)
        # A qualifying two-sided quote posts >= min_size shares on each side,
        # near the mid. Capital committed ~= min_size * (mid + (1-mid)) = min_size.
        qualify_usd = min_size * 1.0  # one YES + one NO share ~= $1 of collateral
        pool = daily_pool(m)
        depth = qualifying_depth(ids[0], mid, max_spread)
        # Upper-bound reward share: your size / (your size + resting qualifying size).
        your_size = min_size
        share = your_size / (your_size + depth) if (your_size + depth) > 0 else 0.0
        est_reward = pool * share
        fits = qualify_usd <= args.account
        if not fits:
            excluded_small += 1
        rows.append({
            "q": str(m.get("question"))[:22],
            "min_size": min_size,
            "max_spread": max_spread,
            "qualify_usd": qualify_usd,
            "pool": pool,
            "depth": depth,
            "share": share,
            "est_reward_upper": est_reward,
            "fits": fits,
        })
        flag = "" if fits else "  (> account)"
        print(f"{str(m.get('question'))[:22]:<22}{min_size:>9.0f}{max_spread:>8.1f}"
              f"{qualify_usd:>18.0f}{flag}")

    if rows:
        affordable = [r for r in rows if r["fits"]]
        print(f"\nOf {len(rows)} reward books probed, the account can afford to quote "
              f"{len(affordable)}.")
        if affordable:
            up = [r["est_reward_upper"] for r in affordable]
            below_min = sum(1 for r in affordable if r["est_reward_upper"] < MIN_DAILY_PAYOUT)
            print(f"UPPER-BOUND daily reward (pool x your share of in-band depth), "
                  f"best case, no adverse selection:")
            print(f"  median ${statistics.median(up):.2f}/day, "
                  f"max ${max(up):.2f}/day across {len(affordable)} markets")
            print(f"  markets where even the UPPER bound is below the ${MIN_DAILY_PAYOUT:.0f}/day "
                  f"payout floor: {below_min}/{len(affordable)}")

    print("\n" + "=" * 68)
    print("What this cannot measure: ADVERSE SELECTION.")
    print("A resting maker is filled exactly when the price is moving against")
    print("them. The reward above is a gross UPPER bound; whether it survives the")
    print("bleed from being picked off cannot be read from a snapshot. That")
    print("requires paper-quoting with honest fills — polybot-v2's maker engine.")
    print("Treat any positive number above as necessary, not sufficient.")

    if args.json:
        payload = {
            "account_usd": args.account,
            "reward_markets": len(reward_markets),
            "by_category": {k: {"n": len(v), "total_daily": sum(v)} for k, v in by_cat.items()},
            "sports_reward_markets": len(sports),
            "probed": rows,
        }
        with open(args.json, "w", encoding="utf-8") as fh:
            json.dump(payload, fh, indent=2)
        print(f"\nWrote {args.json}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
