"""Compute every figure used in the report. Writes numbers.json."""
import json, os, collections, statistics
from urllib.parse import urlparse

HERE = os.path.dirname(os.path.abspath(__file__))
J = lambda n: json.load(open(os.path.join(HERE, n)))


def reg(host):
    p = host.split(".")
    return ".".join(p[-2:]) if len(p) > 2 else host


recs = [json.loads(l) for l in open(os.path.join(HERE, "main_raw.jsonl")) if l.strip()]
cited = J("cited_domains.json")
vendors = J("vendor_domains_merged.json")
CD = {c["domain"]: c for c in cited}

N = {}
MODELS = sorted({r["model"] for r in recs})
CATS = sorted({r["cat"] for r in recs})
N["run_date"] = "2026-09-02"
N["models"] = MODELS
N["n_categories"] = len(CATS)
N["n_calls"] = len(recs)
N["n_errors"] = sum(1 for r in recs if r.get("error"))
N["n_answers_parsed"] = sum(1 for r in recs if r.get("picks"))
N["tranco_list_id"] = "K9QPW"
N["tranco_date"] = "2026-09-01"

# ---- citations ----
allc = collections.Counter()
per_model = {}
for m in MODELS:
    c = collections.Counter()
    for r in recs:
        if r["model"] != m:
            continue
        for u in r.get("citations", []):
            h = urlparse(u).netloc.lower().replace("www.", "")
            if h:
                c[reg(h)] += 1
    allc += c
    tot = sum(c.values())
    unr = sum(n for d, n in c.items() if CD.get(d, {}).get("tranco_rank") is None)
    over = sum(n for d, n in c.items()
               if (CD.get(d, {}).get("tranco_rank") or 10 ** 9) > 100000)
    ranks = []
    for d, n in c.items():
        rk = CD.get(d, {}).get("tranco_rank")
        if rk:
            ranks += [rk] * n
    per_model[m] = {
        "citations": tot, "unique_domains": len(c),
        "pct_unranked": round(100 * unr / tot, 1),
        "pct_worse_than_100k": round(100 * over / tot, 1),
        "median_rank_of_ranked": int(statistics.median(ranks)),
        "top10": c.most_common(10),
    }
N["per_model"] = per_model

tot = sum(allc.values())
N["total_citations"] = tot
N["unique_cited_domains"] = len(allc)
unr = sum(n for d, n in allc.items() if CD.get(d, {}).get("tranco_rank") is None)
over = sum(n for d, n in allc.items()
           if (CD.get(d, {}).get("tranco_rank") or 10 ** 9) > 100000)
N["pct_citations_unranked"] = round(100 * unr / tot, 1)
N["pct_citations_worse_than_100k"] = round(100 * over / tot, 1)
N["n_unranked_domains"] = sum(1 for d in allc if CD.get(d, {}).get("tranco_rank") is None)
N["pct_domains_unranked"] = round(100 * N["n_unranked_domains"] / len(allc), 1)
N["top25_domains"] = [[d, n, round(100 * n / tot, 2), CD.get(d, {}).get("tranco_rank")]
                      for d, n in allc.most_common(25)]
N["top10_share_pct"] = round(100 * sum(n for _, n in allc.most_common(10)) / tot, 1)
_r = []
for d, n in allc.items():
    rk = CD.get(d, {}).get("tranco_rank")
    if rk:
        _r += [rk] * n
N["pooled_median_rank_of_ranked_citations"] = int(statistics.median(_r))
N["n_citations_to_ranked_domains"] = len(_r)

# ---- unranked leaders ----
unranked = [(d, n) for d, n in allc.most_common()
            if CD.get(d, {}).get("tranco_rank") is None]
N["top_unranked"] = [{"domain": d, "citations": n,
                      "categories": CD[d]["categories"],
                      "wayback_first": CD[d]["wayback_first"],
                      "wayback_pages": CD[d]["wayback_pages"]}
                     for d, n in unranked[:30]]

# ---- wayback ----
def year(ts):
    return int(str(ts)[:4]) if ts else None

unr_doms = [d for d, n in unranked]
rk_doms = [d for d, n in allc.most_common() if CD.get(d, {}).get("tranco_rank")]
u_yrs = [year(CD[d]["wayback_first"]) for d in unr_doms if CD[d]["wayback_first"]]
r_yrs = [year(CD[d]["wayback_first"]) for d in rk_doms if CD[d]["wayback_first"]]
N["wayback"] = {
    "unranked_domains": len(unr_doms),
    "unranked_never_archived": sum(1 for d in unr_doms if not CD[d]["wayback_first"]),
    "unranked_never_archived_pct": round(
        100 * sum(1 for d in unr_doms if not CD[d]["wayback_first"]) / len(unr_doms), 1),
    "ranked_domains": len(rk_doms),
    "ranked_never_archived": sum(1 for d in rk_doms if not CD[d]["wayback_first"]),
    "ranked_never_archived_pct": round(
        100 * sum(1 for d in rk_doms if not CD[d]["wayback_first"]) / len(rk_doms), 1),
    "median_first_year_unranked": statistics.median(u_yrs) if u_yrs else None,
    "median_first_year_ranked": statistics.median(r_yrs) if r_yrs else None,
    "unranked_first_seen_2025_or_later_pct": round(
        100 * sum(1 for y in u_yrs if y >= 2025) / len(u_yrs), 1) if u_yrs else None,
    "ranked_first_seen_2025_or_later_pct": round(
        100 * sum(1 for y in r_yrs if y >= 2025) / len(r_yrs), 1) if r_yrs else None,
}

# ---- citation-weighted: share of citations to domains never archived ----
never = sum(n for d, n in allc.items() if not CD.get(d, {}).get("wayback_first"))
N["pct_citations_never_archived"] = round(100 * never / tot, 1)

# ---- model agreement on #1 ----
top1 = collections.defaultdict(dict)
for r in recs:
    ps = r.get("picks") or []
    if ps:
        top1[r["cat"]][r["model"]] = ps[0]["name"].lower().strip()
both = [c for c, d in top1.items() if len(d) == len(MODELS)]
agree = sum(1 for c in both if len(set(top1[c].values())) == 1)
N["agreement"] = {"categories_with_both": len(both), "same_top1": agree,
                  "same_top1_pct": round(100 * agree / len(both), 1)}

# top-5 set Jaccard across models
jac = []
sets = collections.defaultdict(dict)
for r in recs:
    if r.get("picks"):
        sets[r["cat"]][r["model"]] = {p["name"].lower().strip() for p in r["picks"]}
for c, d in sets.items():
    if len(d) == 2:
        a, b = list(d.values())
        jac.append(len(a & b) / len(a | b))
N["agreement"]["mean_top5_jaccard"] = round(sum(jac) / len(jac), 3)
N["agreement"]["n_jaccard_pairs"] = len(jac)

# ---- vendor domains (zombie section) ----
V = {v["domain"]: v for v in vendors}
N["vendors"] = {"unique": len(vendors)}
no_dns = [v["domain"] for v in vendors if not v.get("ip")]
unreach = [v["domain"] for v in vendors if v.get("ip") and v.get("status") is None]
err4xx = [(v["domain"], v["status"]) for v in vendors
          if isinstance(v.get("status"), int) and v["status"] >= 400]


def host_of(url):
    h = urlparse(url).netloc.lower().split("@")[-1].split(":")[0]
    return h[4:] if h.startswith("www.") else h


def offdomain(v):
    """True only if the final URL sits on a different registrable domain."""
    if not v.get("final"):
        return False
    return reg(host_of(v["final"])) != reg(v["domain"])


redir = [(v["domain"], v["final"]) for v in vendors if offdomain(v)]
# 403/429/417/409 are bot-blocking or rate limits, not dead sites: excluded
# from the wrong-or-stale count. Only 404/410 and 5xx count as broken.
broken_http = [(d, s) for d, s in err4xx if s in (404, 410) or s >= 500]
N["vendors"].update({
    "no_dns": sorted(no_dns), "n_no_dns": len(no_dns),
    "dns_but_unreachable": sorted(unreach), "n_unreachable": len(unreach),
    "http_status_counts": sorted(collections.Counter(s for _, s in err4xx).items()),
    "http_broken": sorted(broken_http), "n_http_broken": len(broken_http),
    "n_bot_blocked_403": sum(1 for _, s in err4xx if s == 403),
    "redirect_off_domain": sorted(redir), "n_redirect_off_domain": len(redir),
})
bad = (set(no_dns) | set(unreach) | {d for d, _ in broken_http}
       | {d for d, _ in redir})
N["vendors"]["n_wrong_or_stale"] = len(bad)
N["vendors"]["pct_wrong_or_stale"] = round(100 * len(bad) / len(vendors), 1)
N["vendors"]["n_gone_or_unreachable"] = len(
    set(no_dns) | set(unreach) | {d for d, _ in broken_http})
N["vendors"]["pct_gone_or_unreachable"] = round(
    100 * N["vendors"]["n_gone_or_unreachable"] / len(vendors), 1)

# ---- brands ----
bc = collections.Counter()
for r in recs:
    for p in r.get("picks", []):
        bc[p["name"].lower().strip()] += 1
N["n_distinct_brands"] = len(bc)
N["n_brand_slots"] = sum(bc.values())
N["top_brands"] = bc.most_common(15)

# ---- shared retrieval between the two tiers ----
by_cat = collections.defaultdict(dict)
url_sets = collections.defaultdict(set)
for r in recs:
    by_cat[r["cat"]][r["model"]] = tuple(r.get("citations", []))
    url_sets[r["model"]] |= set(r.get("citations", []))
ident = sum(1 for c, d in by_cat.items()
            if len(d) == len(MODELS) and len(set(d.values())) == 1)
a, b = MODELS
N["shared_retrieval"] = {
    "categories_identical_citation_list": ident,
    "categories_identical_pct": round(100 * ident / len(by_cat), 1),
    "unique_urls_per_model": {m: len(url_sets[m]) for m in MODELS},
    "url_overlap": len(url_sets[a] & url_sets[b]),
    "url_jaccard": round(len(url_sets[a] & url_sets[b])
                         / len(url_sets[a] | url_sets[b]), 3),
    "citations_per_answer_mean": round(tot / len(recs), 2),
}

# ---- the named actors ----
def dom_stats(d):
    c = collections.Counter()
    cats = set()
    urls = set()
    for r in recs:
        for u in r.get("citations", []):
            h = urlparse(u).netloc.lower().replace("www.", "")
            if reg(h) == d:
                c[u] += 1
                cats.add(r["cat"])
                urls.add(u)
    n = sum(c.values())
    return {"domain": d, "citations": n, "pct_of_all": round(100 * n / tot, 2),
            "categories": len(cats), "unique_urls": len(urls),
            "tranco_rank": CD.get(d, {}).get("tranco_rank"),
            "rank_among_domains": [x for x, _ in allc.most_common()].index(d) + 1
            if d in allc else None}

FARM = ["worldmetrics.org", "gitnux.org", "wifitalents.com"]
N["named"] = {d: dom_stats(d) for d in
              ["guideflow.com"] + FARM + ["gartner.com", "g2.com", "reddit.com",
                                          "capterra.com", "wikipedia.org"]}
fn = sum(N["named"][d]["citations"] for d in FARM)
fc = set()
for r in recs:
    for u in r.get("citations", []):
        if reg(urlparse(u).netloc.lower().replace("www.", "")) in FARM:
            fc.add(r["cat"])
N["farm_network"] = {"domains": FARM, "citations": fn,
                     "pct_of_all": round(100 * fn / tot, 2),
                     "categories_touched": len(fc),
                     "categories_touched_pct": round(100 * len(fc) / len(CATS), 1)}

json.dump(N, open(os.path.join(HERE, "numbers.json"), "w"), indent=1)
print(json.dumps({k: v for k, v in N.items()
                  if k not in ("top25_domains", "top_unranked", "per_model",
                               "top_brands", "vendors")}, indent=1))
print("\nper model:")
for m, d in per_model.items():
    print(" ", m, {k: v for k, v in d.items() if k != "top10"})
print("\ntop unranked:")
for u in N["top_unranked"][:20]:
    print(" ", u)
print("\nvendors:", {k: v for k, v in N["vendors"].items()
                     if not isinstance(v, list)})
