"""Reproduce the Polymarket-vs-Kalshi cross-venue arbitrage claim.

Cross-venue arbitrage needs four things at once: the same game quoted on both
venues, real depth on both, an edge, and an edge bigger than both venues' fees.
This measures all four and reports where it breaks. In practice it usually
breaks at step one, which is why the fee arithmetic rarely gets a chance to.

Fee formulas, both taken from the venues' own documentation:

    Polymarket taker   fee = C x rate x p x (1 - p),  sports rate 0.05
    Kalshi taker       fee = ceil(C x 0.07 x P x (1 - P)),  per order, in cents

Kalshi's ceil-per-order is regressive: it rounds a 1-contract order up to a
full cent, which is a 14% surcharge on exactly the size a small account uses.

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

    python research/crossvenue_check.py
    python research/crossvenue_check.py --series KXMLBGAME --json out.json
"""

from __future__ import annotations

import argparse
import json
import math
import re
import sys
import urllib.error
import urllib.request
from typing import Any

GAMMA = "https://gamma-api.polymarket.com"
KALSHI = "https://api.elections.kalshi.com/trade-api/v2"
UA = {"User-Agent": "nulledge-research/1.0 (read-only arbitrage verification)"}

PM_SPORTS_RATE = 0.05
KALSHI_TAKER_RATE = 0.07
CENTS = 100.0

STOPWORDS = {"the", "fc", "vs", "winner"}


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


def pm_fee(price: float) -> float:
    """Polymarket taker fee per share, in dollars."""
    return PM_SPORTS_RATE * price * (1.0 - price)


def kalshi_fee(price: float, contracts: int) -> float:
    """Kalshi taker fee for a whole order, in dollars, rounded up per order."""
    raw = KALSHI_TAKER_RATE * contracts * price * (1.0 - price)
    return math.ceil(raw * CENTS) / CENTS


def norm(text: str) -> set[str]:
    toks = re.findall(r"[a-z]+", text.lower())
    return {t for t in toks if t not in STOPWORDS and len(t) > 2}


def kalshi_series_fee_meta(series: str) -> dict:
    data = fetch(f"{KALSHI}/series/{series}")
    s = data.get("series", data)
    return {
        "ticker": s.get("ticker"),
        "category": s.get("category"),
        "fee_multiplier": s.get("fee_multiplier"),
        "fee_type": s.get("fee_type"),
    }


def kalshi_markets(series: str, limit: int = 200) -> list[dict]:
    data = fetch(f"{KALSHI}/markets?series_ticker={series}&limit={limit}")
    out = []
    for m in data.get("markets", []):
        if m.get("status") != "active":
            continue
        bid, ask = m.get("yes_bid_dollars"), m.get("yes_ask_dollars")
        if bid is None or ask is None:
            continue
        out.append(
            {
                "ticker": m.get("ticker"),
                "title": m.get("title"),
                "sub": m.get("yes_sub_title") or m.get("no_sub_title") or "",
                "bid": float(bid),
                "ask": float(ask),
                # Kalshi reports resting depth in dollars. Zero here means the
                # quote is indicative only -- nothing is actually resting.
                "liquidity": float(m.get("liquidity_dollars") or 0.0),
                "close": str(m.get("close_time"))[:16],
            }
        )
    return out


def pm_sports_markets(limit: int = 100) -> list[dict]:
    ms = fetch(
        f"{GAMMA}/markets?limit={limit}&closed=false"
        f"&order=volume24hr&ascending=false"
    )
    return [m for m in ms if m.get("feeType") == "sports_fees_v2"]


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--series", default="KXMLBGAME")
    ap.add_argument("--json", default=None)
    args = ap.parse_args()

    print("Cross-venue arbitrage feasibility: Polymarket vs Kalshi")
    print("=" * 68)

    meta = kalshi_series_fee_meta(args.series)
    print(f"\nKalshi series {meta['ticker']} ({meta['category']})")
    print(f"  fee_type       : {meta['fee_type']}")
    print(f"  fee_multiplier : {meta['fee_multiplier']}")
    if meta["fee_type"] == "quadratic_with_maker_fees":
        print("  -> This series charges MAKER fees too. 'Quote passively on")
        print("     Kalshi for free' is not available here.")

    km = kalshi_markets(args.series)
    with_depth = [m for m in km if m["liquidity"] > 0]
    print(f"\nKalshi active markets with a quote : {len(km)}")
    print(f"  of those with non-zero resting depth: {len(with_depth)}")

    if km:
        spreads = sorted((m["ask"] - m["bid"]) * CENTS for m in km)
        med = spreads[len(spreads) // 2]
        print(f"  median quoted spread                : {med:.2f} cents")

    pm = pm_sports_markets()
    print(f"Polymarket live sports markets        : {len(pm)}")

    # A funnel, not a match count. Every stage below eliminated a pair that a
    # looser script would have reported as an arbitrage. The first draft of
    # this file matched on two shared tokens and produced nine "profitable"
    # pairs -- all of them artefacts: Los Angeles matching a different LA game,
    # both of Kalshi's per-team markets scored against one Polymarket price,
    # and quotes of 0.001 and 1.000 from already-decided markets.
    pm_index = [(norm(str(m.get("question", ""))), m) for m in pm]

    stage = {
        "kalshi_active": len(km),
        "kalshi_has_depth": 0,
        "team_pair_matched": 0,
        "pm_price_sane": 0,
        "computable": 0,
        "profitable": 0,
    }

    candidates = []
    for k in km:
        if k["liquidity"] <= 0:
            continue
        stage["kalshi_has_depth"] += 1

        # Kalshi titles read "Toronto vs Boston Winner?" -- both teams appear.
        # Require BOTH to be present in the Polymarket question, so a single
        # shared city cannot carry the match on its own.
        kt = norm(f"{k['title']}")
        for pt, m in pm_index:
            shared = kt & pt
            if len(shared) < 2:
                continue
            stage["team_pair_matched"] += 1
            try:
                pm_ask = float(m.get("bestAsk") or 0)
            except (TypeError, ValueError):
                break
            # A book quoting 0.001 or 1.000 is decided, not tradeable.
            if not 0.02 <= pm_ask <= 0.98:
                break
            stage["pm_price_sane"] += 1
            candidates.append((k, m, pm_ask))
            break

    print("\nFeasibility funnel")
    print("-" * 68)
    print(f"  Kalshi active markets quoted        : {stage['kalshi_active']}")
    print(f"  ...with non-zero resting depth      : {stage['kalshi_has_depth']}")
    print(f"  ...matched to a Polymarket game     : {stage['team_pair_matched']}")
    print(f"  ...with a tradeable Polymarket price: {stage['pm_price_sane']}")

    verdict = "no_tradeable_pairs"
    if candidates:
        print(f"\n{'game':<30} {'K ask':>7} {'PM ask':>7} {'gross':>8} {'net':>8}")
        print("-" * 68)
        for k, _m, pm_ask in candidates[:25]:
            gross = 1.0 - (k["ask"] + pm_ask)
            fees = pm_fee(pm_ask) + kalshi_fee(k["ask"], 100) / 100
            net = gross - fees
            stage["computable"] += 1
            if net > 0:
                stage["profitable"] += 1
            print(f"{str(k['title'])[:30]:<30} {k['ask']:>7.3f} {pm_ask:>7.3f} "
                  f"{gross * CENTS:>7.2f}c {net * CENTS:>7.2f}c")
        print(f"\nProfitable after fees: "
              f"{stage['profitable']}/{stage['computable']}")
        verdict = "measured"
    else:
        print("\nNo pair survives the funnel, so there is nothing to price.")
        print("Cross-venue arbitrage needs the same event quoted with real")
        print("depth on both venues simultaneously. That precondition is not")
        print("met right now, and the fee arithmetic never gets a turn.")

    print("\nBreak-even reference at p=0.50, both legs taker:")
    print(f"  Polymarket : {pm_fee(0.50) * CENTS:.2f} c/share")
    print(f"  Kalshi     : {kalshi_fee(0.50, 100) / 100 * CENTS:.2f} c/contract"
          f" at 100 lots")
    print(f"  Kalshi     : {kalshi_fee(0.50, 1) * CENTS:.2f} c on a 1-lot"
          f" (ceil-per-order penalty)")
    total = pm_fee(0.50) + kalshi_fee(0.50, 100) / 100
    print(f"  Two legs   : {total * CENTS:.2f} c -> the two asks must sum"
          f" below {1 - total:.4f}")

    print("\nNote: a quoted price with zero resting depth is not tradeable.")
    print("Any analysis that treats a quote as executable size overstates the")
    print("opportunity -- this is the most common error in arbitrage claims.")

    if args.json:
        with open(args.json, "w", encoding="utf-8") as fh:
            json.dump(
                {
                    "kalshi_series": meta,
                    "kalshi_active": len(km),
                    "kalshi_with_depth": len(with_depth),
                    "polymarket_sports": len(pm),
                    "funnel": stage,
                    "verdict": verdict,
                },
                fh,
                indent=2,
            )
        print(f"\nWrote {args.json}")
    return 0


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