#!/usr/bin/env python3
"""Enumerate candidate wallets from markets resolved in the last N months.

The bulk subgraph is frozen at Jan 2026, so recent data comes from the LIVE
data-api. We can't cheaply page every global trade back 6 months, but we don't
need to: skilled traders concentrate in liquid markets and recur across many of
them. So we source high-volume recently-resolved markets from Gamma, pull each
market's top traders by notional (insider.market_traders), dedup, and tally how
many markets each wallet shows up in (a prioritization signal for scoring).

Output: candidates.json  ->  scored by skill.py.

    python3 enumerate.py            # last 180 days, liquid markets
    python3 enumerate.py 90         # last 90 days
"""

import json
import os
import ssl
import sys
import time
import urllib.request
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import insider  # noqa: E402

GAMMA = "https://gamma-api.polymarket.com/markets"
CTX = ssl._create_unverified_context()
WINDOW_DAYS = int(sys.argv[1]) if len(sys.argv) > 1 else 180
MAX_MARKETS = 1200      # enough high-volume recent markets to surface recurring sharps
MAX_SCAN = 25000        # stop scanning the volume ranking after this many markets
TOP_TRADERS = 30        # top traders by notional per market
MIN_VOLUME = 20000      # $ — focus on liquid markets where skilled traders play


def gamma(params):
    q = "&".join(f"{k}={v}" for k, v in params.items())
    req = urllib.request.Request(f"{GAMMA}?{q}", headers={"User-Agent": "Mozilla/5.0"})
    return json.loads(urllib.request.urlopen(req, timeout=40, context=CTX).read())


def _closed_ts(m):
    """Parse Gamma's closedTime ('2026-05-12 06:44:09+00') -> epoch, else 0."""
    s = m.get("closedTime")
    if not s:
        return 0
    s = s.replace(" ", "T").split("+")[0].split(".")[0]
    try:
        return time.mktime(time.strptime(s, "%Y-%m-%dT%H:%M:%S"))
    except ValueError:
        return insider._parse_end(m.get("endDateIso"))   # fallback to date


def _page(offset, end_min):
    # end_date_min restricts to recently-resolved markets directly, so we don't
    # scan the whole (mostly-old) volume ranking. Retry once on transient error.
    for _ in range(2):
        try:
            return gamma({"limit": 100, "offset": offset, "closed": "true",
                          "end_date_min": end_min, "order": "volume",
                          "ascending": "false"})
        except Exception:
            time.sleep(2)
    return "ERR"


def recent_markets():
    """Markets that resolved within the window (via end_date_min), kept if liquid
    (volumeNum >= MIN_VOLUME and closedTime in window). Gamma is slow (~5-11s/
    page) so we page CONCURRENTLY in waves and stop at end-of-data or MAX_SCAN."""
    cutoff = time.time() - WINDOW_DAYS * 86400
    end_min = time.strftime("%Y-%m-%dT00:00:00Z", time.gmtime(cutoff))
    out, scanned, base, WAVE = [], 0, 0, 4   # low concurrency — gamma throttles bursts
    fail_streak = 0
    with ThreadPoolExecutor(max_workers=WAVE) as ex:
        while len(out) < MAX_MARKETS and scanned < MAX_SCAN:
            pages = list(ex.map(lambda o: _page(o, end_min),
                                [base + i * 100 for i in range(WAVE)]))
            base += WAVE * 100
            ended = got_any = False
            for page in pages:
                if page == "ERR" or page is None:
                    continue                     # skip transient gaps, keep going
                got_any = True
                if len(page) < 100:
                    ended = True                 # genuine end of data
                scanned += len(page)
                for m in page:
                    try:
                        vol = float(m.get("volumeNum") or 0)
                    except (TypeError, ValueError):
                        vol = 0
                    if vol >= MIN_VOLUME and _closed_ts(m) >= cutoff and m.get("conditionId"):
                        out.append((m["conditionId"], m.get("question", "?")[:60]))
            print(f"  scanned {scanned:,}… kept {len(out)}", flush=True)
            if ended:
                break
            # a whole wave erroring out means we've hit gamma's offset ceiling (it
            # 422s past a max offset) — not a transient blip, since _page already
            # retries. Allow one such wave, bail on the second so we never spin.
            if not got_any:
                fail_streak += 1
                if fail_streak >= 2:
                    print("  (gamma returned no more pages — stopping scan)", flush=True)
                    break
            else:
                fail_streak = 0
    return out[:MAX_MARKETS]


def main():
    print(f"sourcing markets resolved in last {WINDOW_DAYS}d (>= ${MIN_VOLUME:,} vol)…",
          flush=True)
    mkts = recent_markets()
    print(f"  {len(mkts)} markets", flush=True)

    seen, name = defaultdict(int), {}

    def grab(cm):
        try:
            cands, _ = insider.market_traders(cm[0], top=TOP_TRADERS)
            return cands
        except Exception:
            return []

    done = 0
    with ThreadPoolExecutor(max_workers=12) as ex:
        for cands in ex.map(grab, mkts):
            for c in cands:
                seen[c["wallet"]] += 1
                name.setdefault(c["wallet"], c["username"])
            done += 1
            if done % 500 == 0:
                print(f"  {done}/{len(mkts)} markets · {len(seen):,} wallets", flush=True)

    # merge with any existing candidates so daily re-runs ACCUMULATE the pool
    path = os.path.join(os.path.dirname(__file__), "candidates.json")
    merged = {}
    if os.path.exists(path):
        for c in json.load(open(path)):
            merged[c["wallet"]] = c
    new = 0
    for w, n in seen.items():
        if w in merged:
            merged[w]["markets_seen"] = max(merged[w].get("markets_seen", 0), n)
            merged[w].setdefault("username", name[w])
        else:
            merged[w] = {"wallet": w, "username": name[w], "markets_seen": n}
            new += 1
    rows = sorted(merged.values(), key=lambda c: c["markets_seen"], reverse=True)
    json.dump(rows, open(path, "w"))
    print(f"{len(rows):,} candidate wallets (+{new} new this run) -> candidates.json "
          f"({sum(1 for r in rows if r['markets_seen'] >= 15):,} seen in >=15 markets)",
          flush=True)


if __name__ == "__main__":
    main()
