"""Accurate per-wallet P&L for Polymarket makers, from the FULL activity ledger.

(read-only; touches no account and places nothing)

WHY NOT /trades. Reconstructing a position from /trades alone is WRONG for
exactly the wallets worth studying. Polymarket lets you SPLIT $1 of collateral
into a Yes+No pair and MERGE it back, and neither is a trade -- so a maker who
mints inventory instead of crossing a spread has trades that look like
unexplained one-sided selling. pm_maker_study.py did this and produced $1.7M on
384 fills, which is not a real number.

/activity carries every cash-affecting event:
    TRADE  SPLIT  MERGE  REDEEM  REWARD  MAKER_REBATE  YIELD
so net cash flow over a closed history IS realised P&L, with no position
arithmetic required at all.

SIGNS. BUY -usdc, SELL +usdc, SPLIT -usdc (pay collateral, receive tokens),
MERGE +usdc (return tokens, receive collateral), REDEEM +usdc (settlement).
Still-open positions are valued from /positions.currentValue.

SUBSIDY IS NOT THE STORY. Kalshi pays makers nothing, so income Polymarket pays
for providing liquidity would not transfer to us -- but measured over six
high-activity wallets it is 0.000%-0.351% of trade volume. Reported separately
so it can be subtracted, but it does not change any ranking.

CROSS-CHECK. /positions carries Polymarket's OWN cashPnl per open position. Two
independent derivations that agree are worth far more than one that cannot be
checked; `pnl_check_delta` is the disagreement and a large value means do not
trust the row.
"""
import collections
import csv
import os
import sys
import time

import requests

DATA = 'https://data-api.polymarket.com'
HERE = os.path.dirname(os.path.abspath(__file__))
IN_CSV = os.path.join(HERE, 'pm_maker_study.csv')
OUT = os.path.join(HERE, 'pm_maker_pnl.csv')
SUBSIDY = {'REWARD', 'MAKER_REBATE', 'YIELD'}


def _get(url, params, tries=4):
    for i in range(tries):
        try:
            r = requests.get(url, params=params, timeout=30)
        except Exception:
            time.sleep(1.2 * (i + 1)); continue
        if r.status_code == 200:
            return r.json()
        if r.status_code == 400:
            return None
        time.sleep(1.2 * (i + 1))
    return None


ACTIVITY_CAP = 5400        # /activity returns HTTP 400 past roughly here


def wallet_pnl(w, max_rows=ACTIVITY_CAP):
    """Cash-flow P&L. `capped` means the history is INCOMPLETE -- do not use it.

    The offset cap is not a nuisance, it is a correctness boundary. With only
    the most recent N events, a SELL or REDEEM whose matching purchase falls
    outside the window has no cost basis and reads as pure profit, so a
    truncated wallet's P&L is biased UPWARD without limit. That is what
    produced an $8.8M row on the first pass.

    So a wallet is only scored when its ENTIRE history fits under the cap.
    That biases the sample toward smaller operators -- the tradeoff is an exact
    number instead of a fictional one.
    """
    cash = 0.0
    subsidy = 0.0
    vol = 0.0
    types = collections.Counter()
    off = 0
    capped = False
    while True:
        if off >= max_rows:
            capped = True
            break
        d = _get(f'{DATA}/activity', {'user': w, 'limit': 500, 'offset': off})
        if d is None:          # HTTP 400 -> we hit the wall, history is longer
            capped = True
            break
        if not d:
            break
        for a in d:
            t = a.get('type')
            try:
                u = float(a.get('usdcSize') or 0)
            except (TypeError, ValueError):
                continue
            types[t] += 1
            if t in SUBSIDY:
                subsidy += u
            elif t == 'TRADE':
                vol += u
                cash += u if (a.get('side') or '').upper() == 'SELL' else -u
            elif t == 'SPLIT':
                cash -= u
            elif t in ('MERGE', 'REDEEM'):
                cash += u
        off += len(d)
        if len(d) < 500:
            break
        time.sleep(0.1)
    # open positions still carry value; /positions is also Polymarket's own P&L
    open_val = 0.0
    own_pnl = 0.0
    pos = _get(f'{DATA}/positions', {'user': w, 'limit': 500}) or []
    for p in pos:
        try:
            open_val += float(p.get('currentValue') or 0)
            own_pnl += float(p.get('cashPnl') or 0)
        except (TypeError, ValueError):
            pass
    return {
        'wallet': w,
        'pnl_spread': round(cash + open_val, 2),
        'pnl_subsidy': round(subsidy, 2),
        'pnl_total': round(cash + open_val + subsidy, 2),
        'trade_volume': round(vol),
        'open_value': round(open_val, 2),
        'pm_cashpnl_open': round(own_pnl, 2),
        'rows': sum(types.values()),
        'splits': types.get('SPLIT', 0), 'merges': types.get('MERGE', 0),
        'redeems': types.get('REDEEM', 0),
        'capped': int(capped),
    }


def main():
    min_fills = int(sys.argv[1]) if len(sys.argv) > 1 else 200
    src = list(csv.DictReader(open(IN_CSV, encoding='utf-8')))
    ws = [r for r in src
          if int(r['fills']) >= min_fills and float(r['maker_share']) >= 0.6]
    print(f'{len(ws)} maker-dominant wallets with >={min_fills} fills\n')
    out = []
    for i, r in enumerate(ws, 1):
        d = wallet_pnl(r['wallet'])
        d['maker_share'] = r['maker_share']
        d['avg_legs_per_event'] = r['avg_legs_per_event']
        d['all3_event_share'] = r['all3_event_share']
        d['fills_sampled'] = r['fills']
        if d['capped']:
            print('  %2d/%d %s  SKIPPED - history exceeds the offset cap'
                  % (i, len(ws), d['wallet'][:14]))
            continue
        out.append(d)
        print('  %2d/%d %s spread=%12.0f subsidy=%8.0f rows=%-6d COMPLETE'
              % (i, len(ws), d['wallet'][:14], d['pnl_spread'], d['pnl_subsidy'],
                 d['rows']))
    out.sort(key=lambda d: -d['pnl_spread'])
    with open(OUT, 'w', newline='', encoding='utf-8') as fh:
        w_ = csv.DictWriter(fh, fieldnames=list(out[0].keys()))
        w_.writeheader(); w_.writerows(out)
    print(f'\nwrote {len(out)} -> {OUT}')
    prof = [d for d in out if d['pnl_spread'] > 0]
    print('\nprofitable on SPREAD alone: %d of %d (%.0f%%)'
          % (len(prof), len(out), 100 * len(prof) / max(len(out), 1)))
    if prof:
        a = sum(float(d['avg_legs_per_event']) for d in prof) / len(prof)
        s3 = sum(float(d['all3_event_share']) for d in prof) / len(prof)
        print('  their mean legs/match      : %.2f  (3.0 = full book like ours)' % a)
        print('  their share of matches all3: %.0f%%' % (100 * s3))
    los = [d for d in out if d['pnl_spread'] <= 0]
    if los:
        a = sum(float(d['avg_legs_per_event']) for d in los) / len(los)
        print('  LOSERS mean legs/match     : %.2f' % a)
    return 0


if __name__ == '__main__':
    sys.exit(main())
