"""AN4.1 stage 2 (DC Analyst 4, 7 Oct 2026): who owns the money and who can pull, at each winner leg start and control point.
v4 coins 21 Sep to 5 Oct (PP wash.db per-wallet swaps). Inputs in S2: sw.csv.gz, coin.csv, liq.csv, nft.csv, ff.csv, cw.csv, btanchor.db, ethusd_hourly.json."""
import pandas as pd, numpy as np, sqlite3, json, bisect, collections, sys, datetime
S2 = sys.argv[1]; ROWS = sys.argv[2]; OUT = sys.argv[3]
PM = "0x58daec3116aae6d93017baaea7749052e8a04fa7"
DEAD = {"0x" + "0" * 40, "0x000000000000000000000000000000000000dead"}
A = sorted(sqlite3.connect(S2 + "/btanchor.db").execute("SELECT b, t FROM a").fetchall())
AB = [b for b, _ in A]
def btime(b):
    i = min(max(bisect.bisect_right(AB, b) - 1, 0), len(A) - 2)
    (b0, t0), (b1, t1) = A[i], A[i + 1]
    return t0 + (t1 - t0) * (b - b0) / (b1 - b0)
def tblock(t):
    T = [x for _, x in A]; i = min(max(bisect.bisect_right(T, t) - 1, 0), len(A) - 2)
    (b0, t0), (b1, t1) = A[i], A[i + 1]
    return int(b0 + (b1 - b0) * (t - t0) / (t1 - t0))
EU = json.load(open(S2 + "/ethusd_hourly.json")); EUT = [a for a, _ in EU]
def eu(t): return EU[min(len(EU) - 1, max(0, bisect.bisect_left(EUT, t)))][1]
rows = pd.read_csv(ROWS)
t21 = int(datetime.datetime(2026, 9, 21, tzinfo=datetime.timezone(datetime.timedelta(hours=7))).timestamp())
rows = rows[(rows.vg == "v4") & (rows.t >= t21)].copy()
coin = pd.read_csv(S2 + "/coin.csv").set_index("token")
sw = pd.read_csv(S2 + "/sw.csv.gz")
sw["wallet"] = sw.wallet.str.lower()
G = {k: g for k, g in sw.groupby("token")}
ff = pd.read_csv(S2 + "/ff.csv"); cw = pd.read_csv(S2 + "/cw.csv")
FUND = {}; FB = {}; KIND = {}
for a, k, f, fb in zip(cw.addr, cw.kind, cw.funder, cw.fund_b):
    if isinstance(f, str): FUND[a.lower()] = f.lower(); FB[a.lower()] = fb
    KIND[a.lower()] = k
for a, k, f, fb in zip(ff.addr, ff.kind, ff.funder, ff.fund_b):
    a = a.lower()
    if isinstance(f, str): FUND[a] = f.lower(); FB[a] = fb
    KIND.setdefault(a, k)
# a funder is an OPERATOR hand for a coin when it funded 3+ buyer wallets of that coin AND at least half of all buyer wallets it ever funded
# (in this coin set) bought this coin. Bot platforms and exchanges spread their wallets over many coins and fail the second test.
FW = collections.defaultdict(set); FWC = collections.defaultdict(lambda: collections.defaultdict(set))
for tok, g in G.items():
    for w in set(g[g.side == 1].wallet):
        f = FUND.get(w)
        if f: FW[f].add(w); FWC[f][tok].add(w)
SERVICE = {f for f, s in FW.items() if len(FWC[f]) >= 30}
print("service funders", len(SERVICE), flush=True)
liq = pd.read_csv(S2 + "/liq.csv"); nft = pd.read_csv(S2 + "/nft.csv")
LQ = {k: g for k, g in liq.groupby("pool")}
OWN = collections.defaultdict(list)
for b, li, tid, too in zip(nft.b, nft.li, nft.tid, nft.too): OWN[int(tid)].append((b, li, str(too).lower()))
def lpclass(pool, blk, creator, crew):
    g = LQ.get(pool)
    if g is None: return "none", np.nan
    pos = collections.defaultdict(float); first = {}
    for b, dl, kind, snd, tid in zip(g.b, g.dl, g.kind, g.sender, g.tid):
        if b >= blk: break
        if kind not in ("add", "pull"): continue
        key = int(tid) if snd == PM and tid == tid else "s:" + str(snd).lower()
        pos[key] += dl
    openk = [k for k, v in pos.items() if v > 1e-6 * max(abs(x) for x in pos.values())] if pos else []
    if not openk: return "empty", 0
    owners = []
    for k in openk:
        if isinstance(k, str): owners.append(("adder", k[2:])); continue
        o = [too for b, li, too in OWN.get(k, []) if b < blk]
        owners.append(("nft", o[-1] if o else "?"))
    if all(kind == "nft" and o in DEAD for kind, o in owners): return "BURN", len(openk)
    if any(o in (creator, crew) for _, o in owners): return "CREATOR", len(openk)
    if any(kind == "adder" for kind, _ in owners): return "ADDER", len(openk)
    return "OTHER", len(openk)
out = []
for r in rows.itertuples():
    tok = r.token.lower(); g = G.get(tok)
    c = coin.loc[tok] if tok in coin.index else None
    creator = str(c.creator).lower() if c is not None else ""; crew = str(c.crew).lower() if c is not None else ""
    blk = tblock(r.t)
    d = dict(pool=r.pool, t=r.t, grp=r.grp)
    lp, npos = lpclass(r.pool, blk, creator, crew); d["lp"] = lp; d["lp_npos"] = npos
    if g is None or len(g) == 0:
        out.append(d); continue
    pre = g[g.b < blk]
    if len(pre) == 0:
        out.append(d); continue
    buys = pre[pre.side == 1]
    # one hand = a non-service funder that funded 3+ distinct buyer wallets of this coin before the point; plus creator / crew funded
    bw = set(buys.wallet)
    fcount = collections.Counter(FUND.get(w) for w in bw if FUND.get(w))
    hands = {f for f, n in fcount.items() if n >= 3 and len(FWC[f][tok]) >= 0.5 * len(FW[f])} | {creator, crew}
    svc = collections.Counter(FUND.get(w) for w in bw if FUND.get(w) in SERVICE)
    d["svc_buyer_share"] = sum(svc.values()) / len(bw) if bw else np.nan
    op = {w for w in set(pre.wallet) if FUND.get(w) in hands or w in (creator, crew)}
    top = collections.Counter({f: n for f, n in fcount.items() if f in hands}).most_common(1)
    d["top_funder_share"] = top[0][1] / len(bw) if top and bw else 0.0
    d["n_hands"] = len(hands - {creator, crew})
    d["ub_all"] = len(bw)
    tot_b = buys.eth.sum()
    d["op_buy_share_all"] = buys[buys.wallet.isin(op)].eth.sum() / tot_b if tot_b else np.nan
    wb = buys.groupby("wallet").eth.sum().sort_values(ascending=False)
    d["top1_buy_share"] = wb.iloc[0] / tot_b if tot_b else np.nan
    d["top10_buy_share"] = wb.iloc[:10].sum() / tot_b if tot_b else np.nan
    fbuy = buys.groupby("wallet").b.min()
    for lab, sec in (("15", 900), ("1h", 3600)):
        lo = tblock(r.t - sec); w = pre[pre.b >= lo]; wbuy = w[w.side == 1]; wsell = w[w.side == -1]
        tb = wbuy.eth.sum(); ts = wsell.eth.sum()
        isop = w.wallet.isin(op)
        ob = wbuy[wbuy.wallet.isin(op)].eth.sum(); os_ = wsell[wsell.wallet.isin(op)].eth.sum()
        e = eu(r.t)
        d["ub" + lab] = wbuy.wallet.nunique()
        d["op_buy_share" + lab] = ob / tb if tb else np.nan
        d["out_net_cap" + lab] = ((tb - ob) - (ts - os_)) * e / r.cap if r.cap else np.nan
        d["op_net_cap" + lab] = (ob - os_) * e / r.cap if r.cap else np.nan
        d["op_sell_share" + lab] = os_ / ts if ts else np.nan
        # fresh = funded within 1 day (about 864k blocks) before its first buy of this coin
        fr = [w_ for w_ in set(wbuy.wallet) if w_ in FB and FB[w_] == FB[w_] and 0 <= fbuy.get(w_, 0) - FB[w_] <= 864000]
        d["fresh_buy_share" + lab] = wbuy[wbuy.wallet.isin(fr)].eth.sum() / tb if tb else np.nan
        d["new_buyers_share" + lab] = (wbuy.wallet.map(fbuy) >= lo).mean() if len(wbuy) else np.nan
    # did the creator / crew sell before the point
    d["creator_sold_eth"] = pre[(pre.side == -1) & pre.wallet.isin({creator, crew})].eth.sum()
    out.append(d)
o = pd.DataFrame(out)
m = rows.merge(o, on=["pool", "t", "grp"], how="left")
m.to_csv(OUT, index=False)
print(m.groupby(["grp", "half"]).size().to_dict())
print(m.groupby("grp").lp.value_counts().unstack(0))
