"""DC2.1 feature build: per triggered coin, per checkpoint (trigger, +15m, +1h, +4h)."""
import pandas as pd, numpy as np, bisect, sys
DELAY = float(sys.argv[1]) if len(sys.argv) > 1 else 0.0
OUTF = sys.argv[2] if len(sys.argv) > 2 else "feat.csv.gz"
R = "/tmp/claude-0/-home-claude/5101628c-bcc2-5fb2-9e13-0e431183bef0/scratchpad/"
DISP = {"0x84157d3d24630c297849c84d49ae7f1805c1b60f", "0x03802649e469ed5680c58dff9e65425e22d21cc5"}
L = pd.read_csv(R + "data/launches_15sep_4oct.csv.gz")
X7 = pd.read_csv(R + "data/crossers500k_7d.csv.gz").set_index("tok").pk7
s = pd.read_csv(R + "raw/sw.csv.gz")
coin = pd.read_csv(R + "raw/coin.csv.gz").set_index("token")
bt = pd.read_csv(R + "raw/btime.csv.gz")
ff = pd.read_csv(R + "raw/ff.csv.gz")
cw = pd.read_csv(R + "raw/cwallet.csv.gz")
cl = pd.read_csv(R + "raw/claunch.csv.gz")
KT, KS, KMAX = bt.t.values, bt.spb.values, bt.k.max()
def btt(b):
    b = np.asarray(b, dtype=np.int64); k = np.minimum(b // 1000, KMAX)
    return KT[k] + (b - k * 1000) * KS[k]
for d in (s, coin): pass
s["wallet"] = s.wallet.str.lower(); s["t"] = btt(s.b.values)
coin["pull_t"] = np.where(coin.pull_b.notna(), btt(coin.pull_b.fillna(0).astype(np.int64).values), np.nan)
DATA_END = s.t.max()
L = L.set_index("tok")
# ethusd per day: launch-file buy USD in first hour / wash buy ETH in first hour
s0 = s.merge(L[["t0"]], left_on="token", right_index=True)
h1 = s0[(s0.side == 1) & (s0.t <= s0.t0 + 3600)].groupby("token").eth.sum()
r = (L.loc[h1.index, "bu_3600"] / h1).replace([np.inf, -np.inf], np.nan)
r = r[(h1 > 0.05)]
EU = r.groupby(L.loc[r.index, "day"]).median()
print("ethusd per day", EU.round(0).to_dict(), flush=True)
# wallet funding
fund = {}
for a, fb, via in zip(ff.addr.str.lower(), ff.fund_b, ff.via):
    fund[a] = (fb, str(via).lower())
for a, fb, via in zip(cw.addr.str.lower(), cw.fund_b, cw.via):
    if a not in fund or pd.isna(fund[a][0]): fund[a] = (fb, str(via).lower())
cwkind = dict(zip(cw.addr.str.lower(), cw.kind))
# creator history
cl["creator"] = cl.creator.str.lower(); cl["t"] = btt(cl.b.values)
cl_by_cr = cl.groupby("creator").t.apply(lambda x: np.sort(x.values)).to_dict()
root_of = cl.drop_duplicates("creator").set_index("creator").root.str.lower().to_dict()
L["cr"] = L.cr.str.lower()
L["root"] = L.cr.map(root_of)
L["t500"] = L.t0 + L.ts_500000; L["t1m"] = L.t0 + L["ts_1e+06"]
past500_cr = L[L.t500.notna()].groupby("cr").t500.apply(lambda x: np.sort(x.values)).to_dict()
past500_root = L[L.t500.notna() & L.root.notna()].groupby("root").t500.apply(lambda x: np.sort(x.values)).to_dict()
# crew label of earlier $1M coins
coin["t1m"] = L.t1m.reindex(coin.index)
crew1m = coin[coin.t1m.notna()].groupby("crew").t1m.apply(lambda x: np.sort(x.values)).to_dict()
# alumni: early (first 30 min) buyers of $1M coins, with that coin's $1M time
g = s.groupby("token")
first_t = g.t.min()
s1 = s.merge(first_t.rename("ft"), left_on="token", right_index=True)
ab = s1[(s1.side == 1) & (s1.t <= s1.ft + 1800)][["token", "wallet"]].drop_duplicates()
ab["t1m"] = ab.token.map(L.t1m)
ab = ab[ab.t1m.notna()]
alum = ab.groupby("wallet").t1m.min().to_dict()  # wallet -> earliest time it became alumni
print("alumni wallets", len(alum), flush=True)
def nb(a, x): return bisect.bisect_right(a, x) if a is not None else 0
CP = [("trig", 0), ("l30", 30), ("m15", 900), ("h1", 3600), ("h4", 14400)]
rows = []
for tok, d in g:
    if tok not in L.index: continue
    lr = L.loc[tok]; day = lr.day; eu = EU.get(day, np.nan)
    d = d.sort_values(["b"]); t = d.t.values; px = d.px.values; side = d.side.values; eth = d.eth.values; w = d.wallet.values
    # usd-cap per unit px: median of mc_X / px at checkpoint
    ks = []
    for X in (60, 300, 900, 1800, 3600):
        mc = lr.get("mc_%d" % X); i = np.searchsorted(t, lr.t0 + X, side="right") - 1
        if pd.notna(mc) and i >= 0 and px[i] > 0: ks.append(mc / px[i])
    if not ks or pd.isna(lr.ts_250000): continue
    K = np.median(ks)
    # unique buyers cumulative
    seen = set(); ub = np.zeros(len(t), int)
    for i in range(len(t)):
        if side[i] == 1: seen.add(w[i])
        ub[i] = len(seen)
    i100 = np.argmax(ub >= 100) if ub[-1] >= 100 else None
    if i100 is None: continue
    t_trig = max(lr.t0 + lr.ts_250000, t[i100])
    itr = np.searchsorted(t, t_trig, side="left")
    if itr >= len(t): continue
    t_trig = t[itr]
    pull_t = coin.pull_t.get(tok, np.nan); crew = coin.crew.get(tok, None)
    usd = eth * eu
    sellable = (side == -1) & (usd >= 100)
    cr = lr.cr; root = lr.root
    cr_fund = fund.get(cr, (np.nan, ""))
    for cpn, off in CP:
        tc = t_trig + off
        if pd.notna(pull_t) and pull_t <= tc: rows.append(dict(tok=tok, cp=cpn, alive=0, day=day)); continue
        if tc > DATA_END: continue
        ic = np.searchsorted(t, tc, side="right") - 1
        pxc = px[ic]; capc = K * pxc; cap_eth = capc / eu
        m = slice(0, ic + 1)
        T, P, S, E, W = t[m], px[m], side[m], eth[m], w[m]
        last1h = T > tc - 3600; last15 = T > tc - 900
        # holdings
        tokn = S * E / P
        hold = pd.Series(tokn).groupby(W).sum()
        pos = hold[hold > 0]
        hold1 = pd.Series(tokn[T <= tc - 3600]).groupby(W[T <= tc - 3600]).sum() if (T <= tc - 3600).any() else pd.Series(dtype=float)
        bmask = S == 1
        bw = pd.Series(E[bmask]).groupby(W[bmask]).sum()
        firstbuy = pd.Series(T[bmask]).groupby(W[bmask]).min()
        fresh_eth = 0.0; farm_n = 0; known = 0
        for wa, e_ in bw.items():
            fb, via = fund.get(wa, (np.nan, ""))
            if pd.notna(fb):
                known += 1
                if firstbuy[wa] - btt([int(fb)])[0] < 3600: fresh_eth += e_
            if via in DISP: farm_n += 1
        al = sum(1 for wa in bw.index if alum.get(wa, 9e18) < tc)
        # impact: virtual ETH reserve from recent swaps
        Pi = P[-31:]; Ei = E[-31:]
        dl = np.abs(np.diff(np.log(Pi))); ev = 2 * Ei[1:] / np.where(dl > 1e-9, dl, np.nan)
        Ev = np.nanmedian(ev) if np.isfinite(ev).any() else np.nan
        # outcomes from entry (tc + DELAY) on
        te = tc + DELAY
        if pd.notna(pull_t) and pull_t <= te: rows.append(dict(tok=tok, cp=cpn, alive=0, day=day)); continue
        ie = np.searchsorted(t, te, side="left")
        if ie >= len(t): continue
        pxe = px[ie]; te = t[ie]
        fut = (t > te) & (t <= te + 7 * 86400)
        sp = px[fut & sellable]
        hit = np.nonzero(fut & sellable & (px >= 5 * pxe))[0]
        t5 = t[hit[0]] if len(hit) else np.nan
        mx = sp.max() / pxe if len(sp) else 0.0
        # value at +24h
        t24 = tc + 86400
        if pd.notna(pull_t) and pull_t <= t24: cap24 = 0.0
        elif t24 > DATA_END: cap24 = np.nan
        else: cap24 = K * px[np.searchsorted(t, t24, side="right") - 1]
        # value at end of 7d hold (pull = 0)
        t7 = tc + 7 * 86400
        if pd.notna(pull_t) and pull_t <= min(t7, DATA_END): end_x = 0.0
        else: end_x = px[np.searchsorted(t, min(t7, DATA_END), side="right") - 1] / pxe
        cpast = cl_by_cr.get(cr)
        rows.append(dict(
            tok=tok, cp=cpn, alive=1, day=day, t0=lr.t0, tc=tc, age_min=(tc - lr.t0) / 60, cap=capc, ethusd=eu, Ev=Ev,
            hfam=lr.hfam, start=lr.start, crew=crew, cr=cr,
            close_high=pxc / P.max(), churn=E[last1h].sum() / cap_eth, bs_ratio=(S[last1h] == 1).sum() / max(1, (S[last1h] == -1).sum()),
            netflow15=(E[last15] * S[last15]).sum() / cap_eth, top10=pos.nlargest(10).sum() / pos.sum() if len(pos) else np.nan,
            holders=len(pos), hold_growth=(len(pos) + 1) / ((hold1 > 0).sum() + 1),
            fresh_share=fresh_eth / bw.sum() if bw.sum() > 0 else np.nan, farm_share=farm_n / max(1, len(bw)), fund_known=known / max(1, len(bw)),
            alumni=al, alumni_share=al / max(1, len(bw)), buyers=len(bw),
            cr_past_n=nb(cpast, lr.t0 - 1) if cpast is not None else 0,
            cr_past500=int(nb(past500_cr.get(cr), tc) > 0), root_past500=int(nb(past500_root.get(root), tc) > 0) if isinstance(root, str) else 0,
            cr_age_h=(lr.t0 - btt([int(cr_fund[0])])[0]) / 3600 if pd.notna(cr_fund[0]) else np.nan,
            cr_disp=int(cr_fund[1] in DISP),
            cr_sold=(E[(W == cr) & (S == -1)]).sum() / cap_eth,
            crew_past1m=int(nb(crew1m.get(crew), tc) > 0) if crew is not None else 0,
            Y5=int(len(hit) > 0), t5=t5, maxx=mx, cap24=cap24, Y1M=(np.nan if pd.isna(cap24) else int(cap24 >= 1e6)), end_x=end_x,
            pk7=X7.get(tok, np.nan), pulled=int(pd.notna(pull_t)), pull_min=(pull_t - tc) / 60 if pd.notna(pull_t) else np.nan,
        ))
F = pd.DataFrame(rows)
F.to_csv(R + OUTF, index=False)
print(F.groupby("cp").agg(n=("tok", "size"), alive=("alive", "sum"), y5=("Y5", "sum"), y1m=("Y1M", "sum")))
