#!/usr/bin/env python3
"""Everything the claim asserts about wallet 0x511aff..e4a5, computed from the chain
pull in events.json and from colour.jsonl. Writes profile.json; the claim text and the
card are rendered from that file, never typed by hand."""
import json, statistics as st, datetime, collections, math, re

W = "0x511affbf7afdf2488fb1cba6e5d19508cff2e4a5"
e = json.load(open("events.json"))
col = {json.loads(l)["id"]: json.loads(l) for l in open("colour.jsonl")}
samp = json.load(open("sample.json"))

def utc(ts): return datetime.datetime.utcfromtimestamp(ts).strftime("%Y-%m-%d")

acc_by_claim = {a["claimId"]: a for a in e["accepted"]}
acc_by_bounty = {a["bountyId"]: a for a in e["accepted"]}
cancelled = {c["bountyId"] for c in e["cancelled"]}

mb = [b for b in e["bounties"] if b["issuer"].lower() == W]
mc = [c for c in e["claims"] if c["issuer"].lower() == W]

# ---------- exact one-sided Mann-Whitney U, no simulation ----------
def mwu_exact(small, big):
    """Exact permutation p for the small group's rank sum, by DP over subset sums.
    Midranks handle ties (doubled so the weights stay integers), so this is exact even
    when two images score the same -- the plain distinct-ranks DP is not."""
    n1 = len(small)
    allv = sorted(small + big)
    N = len(allv)
    mid = {}
    i = 0
    while i < N:                                  # midrank for each tied block
        j = i
        while j + 1 < N and allv[j + 1] == allv[i]:
            j += 1
        mid[allv[i]] = (i + 1 + j + 1)            # = 2 * average of the 1-based ranks
        i = j + 1
    w = [mid[v] for v in allv]                    # doubled midranks, integers
    S = sum(mid[v] for v in small)
    cap = sum(sorted(w)[-n1:])
    dp = [[0] * (cap + 1) for _ in range(n1 + 1)]
    dp[0][0] = 1
    for wi in w:
        for k in range(min(n1, N), 0, -1):
            row, prev = dp[k], dp[k - 1]
            for s in range(cap, wi - 1, -1):
                if prev[s - wi]: row[s] += prev[s - wi]
    tot = sum(dp[n1])
    lo = sum(c for s, c in enumerate(dp[n1]) if s <= S)
    hi = sum(c for s, c in enumerate(dp[n1]) if s >= S)
    U = S / 2 - n1 * (n1 + 1) / 2
    return dict(U=U, rank_sum_x2=S, n1=n1, n2=len(big), total_orderings=tot,
                p_lower=lo / tot, p_upper=hi / tot,
                p_two_sided=min(1.0, 2 * min(lo / tot, hi / tot)))

mine_col = [col[i] for i in samp["mine"]]
base_col = [col[i] for i in samp["sample"]]
metrics = {}
for k in ("colourfulness", "sat", "val", "dark_frac", "bright_frac", "grey_frac"):
    m = [x[k] for x in mine_col]; b = [x[k] for x in base_col]
    try: test = mwu_exact(m, b)
    except ValueError as ex: test = {"error": str(ex)}
    metrics[k] = dict(his=m, his_median=st.median(m), base_median=st.median(b),
                      base_mean=round(st.mean(b), 4),
                      pct_of_his_median=round(100 * sum(1 for x in b if x < st.median(m)) / len(b), 1),
                      test=test)
# "does he avoid mid-brightness" -- distance of mean value from 0.5
m = [round(abs(x["val"] - 0.5), 4) for x in mine_col]
b = [round(abs(x["val"] - 0.5), 4) for x in base_col]
metrics["val_extremity"] = dict(his=m, his_median=st.median(m), base_median=st.median(b),
                                base_mean=round(st.mean(b), 4),
                                pct_of_his_median=round(100 * sum(1 for x in b if x < st.median(m)) / len(b), 1),
                                test=mwu_exact(m, b))

# ---------- money ----------
escrowed = sum(int(b["amount"]) for b in mb)
paid = sum(int(b["amount"]) for b in mb if b["id"] in acc_by_bounty)
returned = sum(int(b["amount"]) for b in mb if b["id"] in cancelled)
still_open = sum(int(b["amount"]) for b in mb
                 if b["id"] not in cancelled and b["id"] not in acc_by_bounty)
# rank among all v1 issuers by ETH escrowed
byissuer = collections.Counter()
for b in e["bounties"]: byissuer[b["issuer"].lower()] += int(b["amount"])
ranked = byissuer.most_common()
rank_escrow = 1 + [a for a, _ in ranked].index(W)
cnt = collections.Counter(b["issuer"].lower() for b in e["bounties"])
rank_count = 1 + sorted(cnt.values(), reverse=True).index(cnt[W])

# earned as a claimant: ClaimAccepted 'amount' is the FEE (2.5%), so payout = fee*39
earned = 0
for c in mc:
    a = acc_by_claim.get(c["id"])
    if a: earned += int(e["bounties"][c["bountyId"]]["amount"]) - int(a["amount"])

# ---------- cadence ----------
acts = sorted([(b["createdAt"], "bounty", b["id"]) for b in mb] +
              [(c["createdAt"], "claim", c["id"]) for c in mc])
days = sorted({utc(t) for t, _, _ in acts})
span = (acts[-1][0] - acts[0][0]) / 86400
gaps = [round((acts[i + 1][0] - acts[i][0]) / 86400, 2) for i in range(len(acts) - 1)]
months = collections.Counter(utc(t)[:7] for t, _, _ in acts)

# ---------- language ----------
btxt = " ".join(b["name"] + " " + b["description"] for b in mb)
ctxt = " ".join(c["name"] + " " + c["uri"] for c in mc)
def lang(txt, docs):
    words = re.findall(r"[A-Za-z']+", txt)
    caps = sum(1 for w in words if w[:1].isupper())
    return dict(chars=len(txt), words=len(words),
                mean_words_per_doc=round(len(words) / max(1, len(docs)), 1),
                capitalised_word_frac=round(caps / max(1, len(words)), 3),
                sentence_case_starts=sum(1 for d in docs if d[:1].isupper()),
                docs=len(docs))
prof = dict(
    wallet=W, generated_utc=datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
    corpus=dict(bounties=len(e["bounties"]), claims=len(e["claims"]),
                accepted=len(e["accepted"]), cancelled=len(e["cancelled"]),
                wallets=len({b["issuer"].lower() for b in e["bounties"]} |
                            {c["issuer"].lower() for c in e["claims"]})),
    made=dict(n=len(mb), escrowed_eth=escrowed / 1e18, paid_out_eth=paid / 1e18,
              reclaimed_eth=returned / 1e18, still_open_eth=still_open / 1e18,
              n_paid=sum(1 for b in mb if b["id"] in acc_by_bounty),
              n_cancelled=sum(1 for b in mb if b["id"] in cancelled),
              rank_by_eth_escrowed=rank_escrow, n_issuers=len(byissuer),
              rank_by_bounty_count=rank_count,
              share_of_all_v1_eth=round(escrowed / sum(byissuer.values()), 4),
              list=[dict(id=b["id"], date=utc(b["createdAt"]), eth=int(b["amount"]) / 1e18,
                         name=b["name"], desc=b["description"],
                         status=("paid" if b["id"] in acc_by_bounty else
                                 "cancelled" if b["id"] in cancelled else "open"))
                    for b in mb]),
    claimed=dict(n=len(mc), n_accepted=sum(1 for c in mc if c["id"] in acc_by_claim),
                 earned_eth=earned / 1e18,
                 list=[dict(id=c["id"], bounty=c["bountyId"], date=utc(c["createdAt"]),
                            name=c["name"], text=c["uri"],
                            accepted=c["id"] in acc_by_claim,
                            bounty_name=e["bounties"][c["bountyId"]]["name"],
                            bounty_eth=int(e["bounties"][c["bountyId"]]["amount"]) / 1e18,
                            image=col[c["id"]]["url"],
                            colour={k: col[c["id"]][k] for k in
                                    ("colourfulness", "sat", "val", "dark_frac",
                                     "bright_frac", "grey_frac", "w", "h", "bytes")})
                       for c in mc]),
    cadence=dict(first=utc(acts[0][0]), last=utc(acts[-1][0]), span_days=round(span, 1),
                 active_days=len(days), n_actions=len(acts),
                 longest_gap_days=max(gaps), median_gap_days=st.median(gaps),
                 by_month=dict(sorted(months.items())), day_list=days),
    language=dict(bounties=lang(btxt, [b["description"] for b in mb]),
                  claims=lang(ctxt, [c["uri"] for c in mc])),
    images=dict(baseline_n=len(base_col), sample_seed=samp["seed"],
                baseline_of=samp["n_others"], metrics=metrics),
)

# ---------- extras the page quotes ----------
bt = json.load(open("block_times.json"))
def iso(b): return datetime.datetime.utcfromtimestamp(bt[str(b)]).strftime("%Y-%m-%d %H:%M UTC")
cl_by_id = {c["id"]: c for c in e["claims"]}
nft = []
for t in e["transfers"]:
    if t["to"].lower() == W or t["from"].lower() == W:
        c = cl_by_id.get(t["tokenId"])
        nft.append(dict(token=t["tokenId"], frm=t["from"], to=t["to"], block=t["block"],
                        when=iso(t["block"]), tx=t["tx"],
                        claim_by=c["issuer"] if c else None,
                        claim_bounty=c["bountyId"] if c else None,
                        bounty_issuer=e["bounties"][c["bountyId"]]["issuer"] if c else None))
CONTRACT = "0xdffe8a4a4103f968ffd61fd082d08c41dcf9b940"
stuck = sum(1 for t in e["transfers"] if t["to"].lower() == CONTRACT)
released = collections.Counter()
last_seen = {}
for t in e["transfers"]:
    last_seen[t["tokenId"]] = t["to"].lower()
still_in_contract = sum(1 for v in last_seen.values() if v == CONTRACT)
# bounties that pay for finding a vulnerability -- hand-checked, listed so the definition is auditable
SEC = [192, 243, 432]
bid = {b["id"]: b for b in e["bounties"]}
sec_tot = sum(int(bid[i]["amount"]) for i in SEC)
sec_his = sum(int(bid[i]["amount"]) for i in SEC if bid[i]["issuer"].lower() == W)
top_funder = ranked[0]
sizes = [b["eth"] for b in prof["made"]["list"]]
prof["extras"] = dict(
    nft_events=nft, nfts_still_in_contract=still_in_contract, nfts_total=len(cl_by_id),
    security_bounty_ids=SEC, security_eth_total=sec_tot / 1e18, security_eth_his=sec_his / 1e18,
    security_share=round(sec_his / sec_tot, 4),
    top_funder_addr=top_funder[0], top_funder_eth=top_funder[1] / 1e18,
    biggest_to_smallest_ratio=round(max(sizes) / min(sizes)),
    top3_of_all=[b["id"] for b in sorted(e["bounties"], key=lambda x: -int(x["amount"]))[:3]],
    last_v1_action=max(t["when"] for t in nft),
    corpus_accept_rate=round(len(e["accepted"]) / len(e["claims"]), 4),
)

json.dump(prof, open("profile.json", "w"), indent=1)
print(json.dumps({k: v for k, v in prof.items() if k in ("made", "cadence", "language")},
                 indent=1, default=str)[:2600])
