"""v2trig.py - PP Feeds #2020 (analyst3 A3-V2Q): trigger + LP-pull watcher for v2 factory 0x8bce pairs, tracked up to 3 h after birth
(v2feed's tape stops at 780 s). Reads pairs.jsonl of v2feed (p0 = first Sync of the birth block), polls Sync logs of the active pair list.
Trigger (once per pair): price=reserve0/reserve1 >= 8 x p0 AND reserve0 >= 2e18 -> v2trig.jsonl. LP pull (once per pair): reserve0 < 0.6 x running max
-> v2lppull.jsonl. Every Sync of active pairs -> v2sync.jsonl. Dedupe on state (trig/pull sets rebuilt from the output files). Flag file v2trig.off stops it."""
import json, os, sys, time
sys.path.insert(0, "/opt/sw"); import rpcfall
D = "/opt/sw/v2feed/trig"; os.makedirs(D, exist_ok=True)
PAIRS = "/opt/sw/v2feed/pairs.jsonl"; TRIG, PULL, SYNC, HB, ST = (D + "/" + n for n in ("v2trig.jsonl", "v2lppull.jsonl", "v2sync.jsonl", "v2trig.hb", "state.json"))
FAC = "0x8bceaa40b9acdfaedf85adf4ff01f5ad6517937f"
T_SYNC = "0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1"
TRACK = 10800; M = 8.0; QMIN = 2e18; PULLF = 0.6; CHUNK = 5000; DRY = "--dry" in sys.argv

def rpc(m, p):
    body = {"jsonrpc": "2.0", "id": 1, "method": m, "params": p}
    try: r = rpcfall.post(body, timeout=15, tries=3)
    except RuntimeError as e:
        if "range limited" not in str(e) or m != "eth_getLogs": raise
        for n in ("drpc_file", "drpc_free"): rpcfall.COOL[n] = time.time() + 300   # bogus 'ranges over 10000' 400 (see v2feed)
        r = rpcfall.post(body, timeout=15, tries=3)
    if "error" in r: raise RuntimeError(str(r["error"])[:150])
    return r.get("result")

def ids(f, k="pair"):
    s = set()
    try:
        for l in open(f):
            try: s.add(json.loads(l)[k])
            except Exception: pass
    except FileNotFoundError: pass
    return s

def out(f, row):
    if DRY: print("WOULD", os.path.basename(f), json.dumps(row)); return
    with open(f, "a") as fh: fh.write(json.dumps(row) + "\n")

def main():
    trig, pulled = ids(TRIG), ids(PULL)
    seed = {}   # pair -> [prev r0, prev r1, sells] rebuilt from v2sync.jsonl so a restart keeps sells_pre
    try:
        for l in open(SYNC):
            try: j = json.loads(l)
            except Exception: continue
            q = seed.setdefault(j["pair"], [None, None, 0])
            if q[0] is not None and j["r0"] < q[0] and j["r1"] > q[1]: q[2] += 1
            q[0], q[1] = j["r0"], j["r1"]
    except FileNotFoundError: pass
    P = {}      # pair -> dict(t0, p0, qmax, tok, blk)
    pos = 0; last = None
    try: last = json.load(open(ST))["last"]
    except Exception: pass
    while not os.path.exists(D + "/v2trig.off"):
        try:
            now = time.time()
            with open(PAIRS) as fh:
                fh.seek(pos)
                for l in fh:
                    if not l.endswith("\n"): break
                    pos += len(l.encode())
                    try: j = json.loads(l)
                    except Exception: continue
                    if j.get("factory") != FAC or not j.get("weth_is_token0") or j["pair"] in P or now - j["t"] > TRACK: continue
                    r0, r1 = j.get("reserve0"), j.get("reserve1")
                    P[j["pair"]] = dict(t0=j["t"], tok=j["token"], blk=j["block"], p0=(r0 / r1) if r0 and r1 else None, qmax=r0 or 0, pr0=r0, pr1=r1, sells=0)
                    if j["pair"] in seed: v = P[j["pair"]]; v["pr0"], v["pr1"], v["sells"] = seed[j["pair"]]
            for a in [a for a, v in P.items() if now - v["t0"] > TRACK]: del P[a]
            head = int(rpc("eth_blockNumber", []), 16)
            if last is None: last = head - int(os.environ.get('BACK', 1))
            while last < head and P:
                hi = min(head, last + CHUNK)
                logs = rpc("eth_getLogs", [{"fromBlock": hex(last + 1), "toBlock": hex(hi), "address": list(P), "topics": [T_SYNC]}]) or []
                for g in sorted(logs, key=lambda g: (int(g["blockNumber"], 16), int(g["logIndex"], 16))):
                    a = g["address"].lower(); v = P.get(a)
                    if not v: continue
                    b = int(g["blockNumber"], 16); r0 = int(g["data"][2:66], 16); r1 = int(g["data"][66:130], 16)
                    if not r1: continue
                    sp = v["sells"]   # sells before this Sync
                    if v["pr0"] is not None and r0 < v["pr0"] and r1 > v["pr1"]: v["sells"] += 1
                    v["pr0"], v["pr1"] = r0, r1
                    px = r0 / r1; row = dict(pair=a, blk=b, r0=r0, r1=r1)
                    out(SYNC, row)
                    if v["p0"] is None: v["p0"] = px
                    v["qmax"] = max(v["qmax"], r0)
                    ts = None
                    if a not in trig and px >= M * v["p0"] and r0 >= QMIN and a not in pulled:
                        trig.add(a); ts = int(rpc("eth_getBlockByNumber", [hex(b), False])["timestamp"], 16)
                        out(TRIG, dict(pair=a, token=v["tok"], birth_blk=v["blk"], birth_t=v["t0"], blk=b, t=ts, age_s=ts - v["t0"], price=px, mult=px / v["p0"], reserve0=r0, sells_pre=sp, emitted_t=time.time()))
                    if a not in pulled and r0 < PULLF * v["qmax"]:
                        pulled.add(a); ts = ts or int(rpc("eth_getBlockByNumber", [hex(b), False])["timestamp"], 16)
                        out(PULL, dict(pair=a, token=v["tok"], blk=b, t=ts, age_s=ts - v["t0"], reserve0=r0, qmax=v["qmax"], triggered=a in trig, emitted_t=time.time()))
                last = hi
            if last is not None and not DRY:
                json.dump({"last": last}, open(ST, "w"))
                json.dump({"t": time.time(), "head": head, "active": len(P), "trig": len(trig), "pulled": len(pulled)}, open(HB, "w"))
        except Exception as e:
            print(time.strftime("%H:%M:%S"), "err", str(e)[:200], flush=True); time.sleep(5)
        if DRY and "--once" in sys.argv: return
        time.sleep(2)

main()
