"""Exact P&L for Polymarket soccer makers, from Polygon logs. (read-only)

WHY ON-CHAIN. The REST route cannot do this. /activity caps at offset ~5400 and
40 of 42 real makers exceed it, and a truncated ledger biases P&L UPWARD without
limit -- a SELL or REDEEM whose purchase falls outside the window has no cost
basis and reads as pure profit (it produced a fake $8.8M row). On-chain there is
no such cap: OrderFilled indexes `maker`, so one wallet's entire fill history is
directly queryable.

MAKER-FIRST, NOT MARKET-FIRST. `assetId` is NOT indexed, and the exchange emits
~46k OrderFilled per 1000 blocks (~2M/day) across all of Polymarket, so scanning
a market's block range and filtering locally is impossible on a public RPC.
Queries must be keyed on the maker address, then filtered to soccer tokens.

DECODING (verified against the API to the cent on
0xfb101b79...fca7e): topics = [sig, orderHash, maker, taker]; data words =
[makerAssetId, takerAssetId, makerAmountFilled, takerAmountFilled, fee, ...].
assetId 0 is USDC, so makerAssetId==0 means the MAKER PAID USDC (bought the
token) and takerAssetId==0 means the maker SOLD it. Amounts are 1e6-scaled.

P&L = net USDC from fills + settlement value of the net token position, where
settlement comes from gamma outcomePrices. A soccer market's whole lifecycle
fits inside the window, so every position has its cost basis -- which is the
property the REST route could not provide.

THE RELIABILITY FLAG. SPLIT/MERGE mint or burn a Yes+No pair for $1 without any
fill, so a wallet that uses them has cash flow this script cannot see. Those
counts are read from /activity and reported as `split_merge`; a row with a large
value is NOT trustworthy and is flagged rather than silently ranked.

STILL NOT ANSWERABLE, whatever we do here: QUOTES. Polymarket's CLOB is
off-chain, so resting orders are public nowhere. Everything below is fills, so
"legs" means legs FILLED, never legs quoted.
"""
import collections
import csv
import json
import os
import sys
import time

import requests

RPC = 'https://polygon-bor-rpc.publicnode.com'
EXCHANGE = '0xe111180000d2663c0091e4f400237545b87b996b'
ORDER_FILLED = '0xd543adfd945773f1a62f74f0ee55a5e3b9b1a28262980ba90b1a89f2ea84d8ee'
GAMMA = 'https://gamma-api.polymarket.com'
DATA = 'https://data-api.polymarket.com'
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, 'pm_onchain_pnl.csv')
CHUNK = 10000           # RPC hard cap on block span
BLOCKS_PER_DAY = 43200  # ~2s blocks


def rpc(method, params, tries=4):
    for i in range(tries):
        try:
            r = requests.post(RPC, json={'jsonrpc': '2.0', 'id': 1,
                                         'method': method, 'params': params},
                              timeout=45)
            j = r.json()
            if 'result' in j:
                return j['result']
        except Exception:
            pass
        time.sleep(1.0 * (i + 1))
    return None


def http(url, params, tries=3):
    for i in range(tries):
        try:
            r = requests.get(url, params=params, timeout=30)
            if r.status_code == 200:
                return r.json()
            if r.status_code == 400:
                return None
        except Exception:
            pass
        time.sleep(1.0 * (i + 1))
    return None


def soccer_tokens():
    """token_id -> (match_slug, leg_question, settlement_price)."""
    tok = {}
    for closed in ('true', 'false'):
        for off in (0, 100, 200, 300):
            evs = http(f'{GAMMA}/events',
                       {'limit': 100, 'offset': off, 'tag_slug': 'soccer',
                        'closed': closed, 'order': 'volume', 'ascending': 'false'})
            if not evs:
                break
            for e in evs:
                ms = e.get('markets') or []
                if len(ms) != 3 or ' vs' not in (e.get('title') or '').lower():
                    continue
                for m in ms:
                    try:
                        ids = json.loads(m.get('clobTokenIds') or '[]')
                        pr = [float(x) for x in json.loads(m.get('outcomePrices') or '[]')]
                    except Exception:
                        continue
                    for i, tid in enumerate(ids):
                        tok[str(int(tid))] = (e.get('slug'), m.get('question'),
                                              pr[i] if i < len(pr) else None)
    return tok


def maker_fills(wallet, from_block, to_block, log=None):
    """Every OrderFilled where this wallet was the MAKER, decoded."""
    topic = '0x' + '0' * 24 + wallet[2:].lower()
    out = []
    b = from_block
    while b < to_block:
        hi = min(b + CHUNK, to_block)
        res = rpc('eth_getLogs', [{'fromBlock': hex(b), 'toBlock': hex(hi),
                                   'address': EXCHANGE,
                                   'topics': [ORDER_FILLED, None, topic]}])
        for l in (res or []):
            d = l['data'][2:]
            w = [d[i:i + 64] for i in range(0, len(d), 64)]
            if len(w) < 4:
                continue
            mk_asset, tk_asset = str(int(w[0], 16)), str(int(w[1], 16))
            mk_amt, tk_amt = int(w[2], 16) / 1e6, int(w[3], 16) / 1e6
            if mk_asset == '0':          # maker paid USDC -> BOUGHT tk_asset
                out.append((tk_asset, +tk_amt, -mk_amt))
            elif tk_asset == '0':        # maker delivered token -> SOLD it
                out.append((mk_asset, -mk_amt, +tk_amt))
        b = hi
        time.sleep(0.05)
    return out


def main():
    topn = int(sys.argv[1]) if len(sys.argv) > 1 else 20
    pad_d = float(sys.argv[2]) if len(sys.argv) > 2 else 2.0
    head = int(rpc('eth_blockNumber', []), 16)

    tok = soccer_tokens()
    print(f'soccer tokens mapped: {len(tok)}')

    # THE WINDOW MUST COVER WHEN THE SOCCER MATCHES WERE LIVE, not "recently".
    # Scanning the last N days found 13,720 fills for one candidate and ZERO in
    # soccer: these are generalist makers whose current book is elsewhere, and
    # the matches in the sample settled months ago. Derive the range from actual
    # soccer trade timestamps instead, padded either side so opening and
    # settlement fills are inside it.
    ts = []
    seen = 0
    # sample trade timestamps across the mapped soccer markets
    for tid_slug in list({v[0] for v in tok.values()})[:14]:
        d = http(f'{GAMMA}/events', {'slug': tid_slug})
        if not d:
            continue
        for e in d:
            for m in (e.get('markets') or []):
                tr = http(f'{DATA}/trades', {'market': m.get('conditionId'),
                                             'limit': 1, 'takerOnly': 'false'})
                if tr:
                    ts.append(int(tr[0]['timestamp']))
                tr2 = http(f'{DATA}/trades', {'market': m.get('conditionId'),
                                              'limit': 1, 'offset': 400,
                                              'takerOnly': 'false'})
                if tr2:
                    ts.append(int(tr2[0]['timestamp']))
                seen += 1
        if seen > 20:
            break
    if not ts:
        print('could not date the soccer markets'); return 1
    now = int(time.time())
    hi_ts, lo_ts = max(ts), min(ts)
    frm = head - int((now - lo_ts + pad_d * 86400) / 2)
    to = head - int(max(0, (now - hi_ts - pad_d * 86400)) / 2)
    print('soccer activity spans %s .. %s' % (time.strftime('%Y-%m-%d', time.gmtime(lo_ts)),
                                              time.strftime('%Y-%m-%d', time.gmtime(hi_ts))))
    print('blocks %d..%d  (%d calls/wallet)' % (frm, to, (to - frm) // CHUNK))
    head = to

    src = list(csv.DictReader(open(os.path.join(HERE, 'pm_maker_study.csv'),
                                   encoding='utf-8')))
    cands = [r for r in src if float(r['maker_share']) >= 0.6]
    cands.sort(key=lambda r: -int(r['fills']))
    cands = cands[:topn]
    print(f'candidate makers: {len(cands)}\n')

    rows = []
    for i, c in enumerate(cands, 1):
        w = c['wallet']
        fills = maker_fills(w, frm, head)
        cash = 0.0
        pos = collections.defaultdict(float)
        n_soccer = 0
        legs = collections.defaultdict(set)
        for tid, dq, dcash in fills:
            if tid not in tok:
                continue
            n_soccer += 1
            cash += dcash
            pos[tid] += dq
            slug, q, _ = tok[tid]
            legs[slug].add(q)
        settle = 0.0
        unresolved = 0
        for tid, q in pos.items():
            p = tok[tid][2]
            if p is None:
                unresolved += 1
            else:
                settle += q * p
        act = http(f'{DATA}/activity', {'user': w, 'limit': 500}) or []
        sm = sum(1 for a in act if a.get('type') in ('SPLIT', 'MERGE'))
        nlegs = [len(v) for v in legs.values()]
        rows.append({
            'wallet': w,
            'pnl_soccer': round(cash + settle, 2),
            'soccer_fills': n_soccer,
            'matches': len(legs),
            'mean_legs_filled': round(sum(nlegs) / len(nlegs), 2) if nlegs else 0,
            'all3_share': round(sum(1 for x in nlegs if x == 3) / len(nlegs), 2) if nlegs else 0,
            'total_fills_window': len(fills),
            'unresolved_tokens': unresolved,
            'split_merge_recent': sm,
            'reliable': int(sm == 0 and unresolved == 0),
        })
        print('  %2d/%d %s soccer_fills=%-6d matches=%-4d pnl=%12.2f %s'
              % (i, len(cands), w[:14], n_soccer, len(legs), cash + settle,
                 '' if rows[-1]['reliable'] else '(flagged)'))

    rows.sort(key=lambda r: -r['pnl_soccer'])
    with open(OUT, 'w', newline='', encoding='utf-8') as fh:
        wtr = csv.DictWriter(fh, fieldnames=list(rows[0].keys()))
        wtr.writeheader(); wtr.writerows(rows)
    print(f'\nwrote {len(rows)} -> {OUT}')
    rel = [r for r in rows if r['reliable'] and r['soccer_fills'] >= 20]
    print('\nRELIABLE ROWS (no split/merge, all positions settled, >=20 fills): %d' % len(rel))
    for r in rel[:12]:
        print('  %-44s pnl=%11.2f fills=%-6d matches=%-4d legs=%.2f all3=%.0f%%'
              % (r['wallet'], r['pnl_soccer'], r['soccer_fills'], r['matches'],
                 r['mean_legs_filled'], 100 * r['all3_share']))
    if rel:
        p = [r for r in rel if r['pnl_soccer'] > 0]
        print('\n  profitable: %d of %d (%.0f%%)' % (len(p), len(rel), 100 * len(p) / len(rel)))
        if p:
            print('  profitable makers: mean legs filled %.2f, all3 %.0f%%'
                  % (sum(r['mean_legs_filled'] for r in p) / len(p),
                     100 * sum(r['all3_share'] for r in p) / len(p)))
        l = [r for r in rel if r['pnl_soccer'] <= 0]
        if l:
            print('  losing makers    : mean legs filled %.2f'
                  % (sum(r['mean_legs_filled'] for r in l) / len(l)))
    return 0


if __name__ == '__main__':
    sys.exit(main())
