#!/usr/bin/env python3
"""从 SNAP Reddit Hyperlink Network（body）构建本轮使用的真实时变有符号图。

选点、分窗、训练/留出切分三条规则都在任何干预实验之前固定，且与实验结果无关：

  * 节点 = 全时段「关联负情感边（LINK_SENTIMENT=-1）条数」最多的 250 个 subreddit。
    并列按名字升序，确定性。用负情感边而不是总活跃度选点，是因为需求方要的是极化话题子图，
    负情感边是这个数据集自带的冲突信号。
  * 再取训练期诱导子图的最大连通分量，避免孤立点让极化方差的定义退化。
  * 边流 = 两端都在节点集里的全部超链接边（正负都要），保留原始时间戳与符号。
  * 时间窗 = 自然月。2013-12 只有 1 天数据，丢弃；保留 2014-01 .. 2017-04 共 40 个月窗。
  * 前 60%（24 个月，2014-01..2015-12）为训练期；后 40%（16 个月，2016-01..2017-04）
    整段留出，离线 RL 的训练数据一条都不来自留出期。

本脚本**不产生任何实验结果**，只产生环境。它跑在 claims.json 写死之前。
"""
from __future__ import annotations

import json
import sys
from collections import Counter, defaultdict
from pathlib import Path

import numpy as np
import scipy.sparse as sp
import scipy.sparse.csgraph as csgraph

RAW = Path(sys.argv[1])
OUT = Path(sys.argv[2])
OUT.mkdir(parents=True, exist_ok=True)

N_SEED_NODES = 250
TRAIN_FRACTION = 0.6
DROP_MONTHS = {"2013-12"}


def main() -> int:
    neg_deg = Counter()
    rows = []
    with (RAW / "soc-redditHyperlinks-body.tsv").open(encoding="utf-8") as fh:
        header = next(fh).rstrip("\n").split("\t")
        assert header[:5] == ["SOURCE_SUBREDDIT", "TARGET_SUBREDDIT", "POST_ID",
                              "TIMESTAMP", "LINK_SENTIMENT"], header
        for line in fh:
            p = line.split("\t", 6)
            u, v, ts, s = p[0], p[1], p[3], int(p[4])
            if u == v or ts[:7] in DROP_MONTHS:
                continue
            rows.append((u, v, ts, s))
            if s < 0:
                neg_deg[u] += 1
                neg_deg[v] += 1

    seed_names = [n for n, _ in sorted(neg_deg.items(), key=lambda kv: (-kv[1], kv[0]))][:N_SEED_NODES]
    seed_idx = {n: i for i, n in enumerate(seed_names)}
    months = sorted({ts[:7] for _, _, ts, _ in rows})
    mindex = {m: t for t, m in enumerate(months)}
    T = len(months)
    n_train = int(round(T * TRAIN_FRACTION))

    # 训练期诱导子图的最大连通分量
    M = len(seed_names)
    adj = np.zeros((M, M), dtype=np.int64)
    for u, v, ts, _ in rows:
        if u in seed_idx and v in seed_idx and mindex[ts[:7]] < n_train:
            adj[seed_idx[u], seed_idx[v]] += 1
    ncomp, labels = csgraph.connected_components(sp.csr_matrix(adj != 0), directed=False)
    keep = labels == np.bincount(labels).argmax()
    names = [seed_names[i] for i in range(M) if keep[i]]
    idx = {n: i for i, n in enumerate(names)}
    N = len(names)

    per_window: dict[int, list] = defaultdict(list)
    for u, v, ts, s in rows:
        if u in idx and v in idx:
            per_window[mindex[ts[:7]]].append((idx[u], idx[v], s, ts))
    for t in range(T):
        per_window[t].sort(key=lambda e: e[3])      # 窗内按真实时间戳排序

    edges = {f"w{t}": np.array([(u, v, s) for u, v, s, _ in per_window[t]],
                               dtype=np.int64).reshape(-1, 3) for t in range(T)}
    np.savez_compressed(OUT / "temporal_graph.npz", names=np.array(names),
                        months=np.array(months), n_train=n_train, **edges)

    kept = sum(len(per_window[t]) for t in range(T))
    kneg = sum(1 for t in range(T) for _, _, s, _ in per_window[t] if s < 0)
    deg_all = Counter()
    for t in range(T):
        for u, v, _, _ in per_window[t]:
            deg_all[names[u]] += 1
            deg_all[names[v]] += 1
    summary = {
        "dataset": "SNAP soc-redditHyperlinks-body.tsv — Reddit Hyperlink Network, Kumar et al., WWW 2018",
        "download": "https://snap.stanford.edu/data/soc-redditHyperlinks-body.tsv（匿名游客直连，无需注册 / API key / DUA）",
        "sha256_body_tsv": "9bbf57511dc0da7369c2e896d865b68f170136def4f223f4c9e9e10818f4bfb5",
        "raw_edges_after_dropping_selfloops_and_partial_month": len(rows),
        "raw_distinct_subreddits": len(neg_deg),
        "node_selection_rule": "关联负情感边条数最多的 250 个 subreddit（并列按名字升序），再取训练期诱导子图最大连通分量",
        "n_seed_nodes": N_SEED_NODES,
        "n_components_in_seed_subgraph": int(ncomp),
        "n_nodes": N,
        "induced_edges": kept,
        "induced_negative_edges": kneg,
        "induced_negative_fraction": round(kneg / kept, 6),
        "n_windows": T, "window_unit": "自然月",
        "n_train_windows": n_train, "n_eval_windows": T - n_train,
        "train_window_range": [months[0], months[n_train - 1]],
        "eval_window_range": [months[n_train], months[-1]],
        "edges_per_window": {months[t]: len(per_window[t]) for t in range(T)},
        "eval_period_edges": sum(len(per_window[t]) for t in range(n_train, T)),
        "top15_nodes_by_induced_degree": [n for n, _ in deg_all.most_common(15)],
        "note_no_opinion_axis": (
            "本数据集不含任何节点观点标签。我们检验过用有符号邻接/有符号拉普拉斯提取「两大阵营轴」："
            "该图远不是结构平衡的（归一化有符号拉普拉斯最小特征值 0.055，不接近 0），"
            "主特征向量基本由节点度数主导、247 个节点里只有 3 个落在另一侧。"
            "因此本轮**不**伪造一个政治立场轴，意见值按 SCOPE 里写明的操作定义作为受控状态变量，"
            "极化由真实的负情感边与真实时序驱动而产生。"),
    }
    (OUT / "graph_summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1),
                                            encoding="utf-8")
    print(json.dumps({k: v for k, v in summary.items() if k != "edges_per_window"},
                     ensure_ascii=False, indent=1))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
