"""Reproduce the fee and order-book claims behind docs/SPORTS_EDGE_RESEARCH.md.

Every number this prints is regenerated from live Polymarket APIs. Nothing is
hardcoded from the research notes -- if Polymarket changes a fee rate, this
script disagrees with the document, and the document is the thing that is wrong.

Stdlib only, no install step, read-only. Never authenticates, never orders.

    python research/polymarket_sports_fee_check.py
    python research/polymarket_sports_fee_check.py --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 fee verification)"}

# Sports markets we care about. Non-sports categories are collected for contrast
# because the whole argument turns on sports being the expensive one.
SPORTS_FEE_TYPE = "sports_fees_v2"

# A binary contract pays $1. One probability point == one cent == $0.01.
CENTS = 100.0


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 taker_fee_per_share(rate: float, price: float) -> float:
    """Polymarket's documented taker fee: fee = C x feeRate x p x (1 - p).

    Returned per single share, in dollars. The p(1-p) term is Bernoulli
    variance, so the fee is a bell curve peaking at a 50/50 price.
    """
    return rate * price * (1.0 - price)


def collect_fee_schedules(limit: int = 300) -> dict[str, dict]:
    """Group live markets by feeType and record the schedule each one carries."""
    markets = fetch(
        f"{GAMMA}/markets?limit={limit}&closed=false"
        f"&order=volume24hr&ascending=false"
    )
    by_type: dict[str, dict] = {}
    for m in markets:
        fee_type = m.get("feeType") or "(none)"
        schedule = m.get("feeSchedule")
        entry = by_type.setdefault(
            fee_type, {"count": 0, "schedules": set(), "example": None}
        )
        entry["count"] += 1
        entry["schedules"].add(json.dumps(schedule, sort_keys=True))
        if entry["example"] is None:
            entry["example"] = str(m.get("question"))[:60]
    return by_type, markets


def sports_markets(markets: list[dict]) -> list[dict]:
    return [m for m in markets if m.get("feeType") == SPORTS_FEE_TYPE]


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 book(token_id: str) -> dict | None:
    try:
        return fetch(f"{CLOB}/book?token_id={token_id}", timeout=20)
    except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError):
        return None


def best_levels(b: dict) -> tuple[float | None, float | None, float, float]:
    """Return (best_bid, best_ask, bid_size, ask_size) from a CLOB book.

    Polymarket returns bids ascending and asks descending, so the best of each
    is the LAST element, not the first. Getting this backwards silently
    produces an inverted spread that looks like free money.
    """
    bids = b.get("bids") or []
    asks = b.get("asks") or []
    bid = float(bids[-1]["price"]) if bids else None
    ask = float(asks[-1]["price"]) if asks else None
    bid_sz = float(bids[-1]["size"]) if bids else 0.0
    ask_sz = float(asks[-1]["size"]) if asks else 0.0
    return bid, ask, bid_sz, ask_sz


def check_mirrored_book(market: dict) -> dict | None:
    """Test whether YES and NO are one book or two.

    If they are one mirrored book then YES_ask == 1 - NO_bid and the resting
    sizes are identical, which makes 'YES ask + NO ask < 1.00' arithmetically
    impossible rather than merely rare.
    """
    ids = token_ids(market)
    if len(ids) != 2:
        return None
    b_yes, b_no = book(ids[0]), book(ids[1])
    if not b_yes or not b_no:
        return None

    yes_bid, yes_ask, _, yes_ask_sz = best_levels(b_yes)
    no_bid, no_ask, no_bid_sz, _ = best_levels(b_no)
    if None in (yes_bid, yes_ask, no_bid, no_ask):
        return None

    return {
        "question": str(market.get("question"))[:60],
        "yes_bid": yes_bid,
        "yes_ask": yes_ask,
        "no_bid": no_bid,
        "no_ask": no_ask,
        "yes_ask_plus_no_ask": round(yes_ask + no_ask, 6),
        "mid_sum": round((yes_bid + yes_ask) / 2 + (no_bid + no_ask) / 2, 6),
        # The mirror test: does the YES ask correspond exactly to the NO bid?
        "complement_gap": round(abs(yes_ask - (1.0 - no_bid)), 6),
        "size_match": abs(yes_ask_sz - no_bid_sz) < 1e-6,
        "spread_cents": round((yes_ask - yes_bid) * CENTS, 3),
    }


def breakeven_table(rate: float) -> list[dict]:
    rows = []
    for p in (0.50, 0.70, 0.80, 0.90, 0.95):
        fee = taker_fee_per_share(rate, p)
        rows.append(
            {
                "price": p,
                "fee_cents_per_share": round(fee * CENTS, 4),
                "fee_pct_of_stake": round(fee / p * 100, 3),
                # Two taker legs at complementary prices share the same p(1-p).
                "two_leg_max_sum": round(1.0 - 2 * fee, 5),
            }
        )
    return rows


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--limit", type=int, default=300, help="markets to sample")
    ap.add_argument("--books", type=int, default=25, help="sports books to probe")
    ap.add_argument("--json", type=str, default=None, help="write results here")
    args = ap.parse_args()

    print("Polymarket sports fee + order-book verification")
    print("=" * 68)

    by_type, markets = collect_fee_schedules(args.limit)
    print(f"\nSampled {len(markets)} live markets by 24h volume.\n")
    print(f"{'feeType':<22} {'n':>4}  schedule")
    print("-" * 68)
    sports_rate = None
    for fee_type, e in sorted(by_type.items(), key=lambda kv: -kv[1]["count"]):
        for sched_json in sorted(e["schedules"]):
            sched = json.loads(sched_json)
            print(f"{fee_type:<22} {e['count']:>4}  {sched}")
            if fee_type == SPORTS_FEE_TYPE and sched:
                sports_rate = sched.get("rate")

    if sports_rate is None:
        print("\nNo sports fee schedule found in the sample. Cannot continue.")
        return 1

    print(f"\nSports taker rate: {sports_rate}")
    print("Break-even for a taker, fee = C x rate x p x (1-p):\n")
    print(f"{'price':>6} {'fee c/share':>12} {'% of stake':>11} {'2-leg sum <':>12}")
    print("-" * 45)
    rows = breakeven_table(sports_rate)
    for r in rows:
        print(
            f"{r['price']:>6.2f} {r['fee_cents_per_share']:>12.3f} "
            f"{r['fee_pct_of_stake']:>10.2f}% {r['two_leg_max_sum']:>12.5f}"
        )

    sm = sports_markets(markets)
    print(f"\nProbing order books for {min(args.books, len(sm))} sports markets...")
    checks = []
    for m in sm[: args.books]:
        c = check_mirrored_book(m)
        if c:
            checks.append(c)

    if not checks:
        print("No usable books returned.")
        return 1

    spreads = sorted(c["spread_cents"] for c in checks)
    gaps = [c["complement_gap"] for c in checks]
    sums = [c["yes_ask_plus_no_ask"] for c in checks]
    mirrored = sum(1 for c in checks if c["complement_gap"] < 1e-9)
    size_matched = sum(1 for c in checks if c["size_match"])
    below_one = sum(1 for s in sums if s < 1.0)

    print(f"\nBooks analysed: {len(checks)}")
    print(f"  median spread                 : {statistics.median(spreads):.2f} cents")
    print(f"  YES_ask == 1 - NO_bid exactly : {mirrored}/{len(checks)}")
    print(f"  resting sizes identical       : {size_matched}/{len(checks)}")
    print(f"  max |complement gap|          : {max(gaps):.2e}")
    print(f"  median (YES ask + NO ask)     : {statistics.median(sums):.5f}")
    print(f"  books with YES+NO ask < 1.00  : {below_one}/{len(checks)}")

    verdict = (
        "ONE MIRRORED BOOK -- intra-market YES+NO arbitrage is structurally "
        "impossible"
        if mirrored == len(checks)
        else "books diverge -- re-examine the mirror assumption"
    )
    print(f"\nVerdict: {verdict}")

    if args.json:
        payload = {
            "sports_rate": sports_rate,
            "fee_types": {
                k: {"count": v["count"], "schedules": sorted(v["schedules"])}
                for k, v in by_type.items()
            },
            "breakeven": rows,
            "books": checks,
            "summary": {
                "n_books": len(checks),
                "median_spread_cents": statistics.median(spreads),
                "mirrored_exactly": mirrored,
                "sizes_identical": size_matched,
                "median_ask_sum": statistics.median(sums),
                "books_below_one": below_one,
            },
        }
        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())
