import json, itertools, collections, sys
import networkx as nx
from networkx.algorithms import bipartite
sys.path.insert(0, '.')
from orgs import ORGS, EDGES
from ties import T, UNVERIFIED

HAARA = {k: v[2] for k, v in ORGS.items()}
cols = ["henkilo","puolue","plahde","taso","asema","org","rooli","kausi","hyoty","summa","tier","lahde","varmuus"]
ties = [dict(zip(cols, t)) for t in T]

# persons
persons = {}
for t in ties:
    p = persons.setdefault(t["henkilo"], {"puolue": set(), "tiers": set(), "orgs": set(), "tasot": set()})
    p["puolue"].add(t["puolue"]); p["tiers"].add(t["tier"]); p["orgs"].add(t["org"]); p["tasot"].add(t["taso"])
conflicts = {n: p["puolue"] for n, p in persons.items() if len(p["puolue"]) > 1}
for n, p in persons.items():
    p["party"] = sorted(p["puolue"])[0]
    p["mintier"] = min(x for x in p["tiers"]) if p["tiers"] else None

# graphs
GB = nx.Graph()
for n, p in persons.items():
    GB.add_node("P:"+n, kind="person", party=p["party"])
for t in ties:
    o = "O:"+t["org"]
    GB.add_node(o, kind="org", haara=HAARA[t["org"]])
    u, v = "P:"+t["henkilo"], o
    if GB.has_edge(u, v):
        GB[u][v]["weight"] += 1; GB[u][v]["tiers"].add(t["tier"])
    else:
        GB.add_edge(u, v, weight=1, tiers={t["tier"]}, layer="sidos")
GF = GB.copy()
for a, b, typ, desc in EDGES:
    for x in (a, b):
        if "O:"+x not in GF:
            GF.add_node("O:"+x, kind="org", haara=HAARA[x])
    if not GF.has_edge("O:"+a, "O:"+b):
        GF.add_edge("O:"+a, "O:"+b, weight=1, layer=typ)

P_nodes = [n for n, d in GB.nodes(data=True) if d["kind"] == "person"]
O_nodes_B = [n for n, d in GB.nodes(data=True) if d["kind"] == "org"]
res = {}
res["n_persons"] = len(P_nodes)
res["n_ties"] = len(ties)
res["n_orgs_with_politicians"] = len(O_nodes_B)
res["n_orgs_total"] = sum(1 for _, d in GF.nodes(data=True) if d["kind"] == "org")
res["party_conflicts"] = {k: sorted(v) for k, v in conflicts.items()}
res["persons_by_party"] = collections.Counter(persons[n[2:]]["party"] for n in P_nodes)
res["persons_by_mintier"] = collections.Counter(persons[n[2:]]["mintier"] for n in P_nodes)
res["ties_by_tier"] = collections.Counter(t["tier"] for t in ties)
pt = collections.defaultdict(collections.Counter)
for n in P_nodes:
    p = persons[n[2:]]
    for tr in p["tiers"]:
        pt[tr][p["party"]] += 1
res["party_by_tier"] = {k: dict(v) for k, v in pt.items()}

# degree in bipartite, haarat spanned
deg = []
for n in P_nodes:
    orgs = sorted(o[2:] for o in GB[n])
    haarat = sorted({HAARA[o] for o in orgs})
    deg.append((n[2:], persons[n[2:]]["party"], len(orgs), len(haarat), orgs, haarat))
deg.sort(key=lambda x: (-x[2], -x[3], x[0]))
res["multi_org_persons"] = [d for d in deg if d[2] >= 2]
res["org_degree"] = sorted(((o[2:], GB.degree(o)) for o in O_nodes_B), key=lambda x: -x[1])

# org projection (interlocks)
OP = bipartite.weighted_projected_graph(GB, O_nodes_B)
res["interlocks"] = sorted(((a[2:], b[2:], d["weight"]) for a, b, d in OP.edges(data=True)), key=lambda x: -x[2])

# centralities on full graph
lcc = max(nx.connected_components(GF), key=len)
G = GF.subgraph(lcc).copy()
res["lcc_size"] = len(G); res["full_size"] = len(GF)
btw = nx.betweenness_centrality(G, normalized=True)
eig = nx.eigenvector_centrality_numpy(G)
core = nx.core_number(G)
res["top_btw_persons"] = sorted(((n[2:], round(btw[n], 4)) for n in G if n.startswith("P:")), key=lambda x: -x[1])[:15]
res["top_btw_orgs"] = sorted(((n[2:], round(btw[n], 4)) for n in G if n.startswith("O:")), key=lambda x: -x[1])[:15]
res["top_eig_persons"] = sorted(((n[2:], round(eig[n], 4)) for n in G if n.startswith("P:")), key=lambda x: -x[1])[:10]
res["top_eig_orgs"] = sorted(((n[2:], round(eig[n], 4)) for n in G if n.startswith("O:")), key=lambda x: -x[1])[:10]
kmax = max(core.values())
res["kmax"] = kmax
res["core_dist"] = collections.Counter(core.values())
res["max_core_members"] = sorted(n for n in G if core[n] == kmax)

# robustness check: betweenness without vote-funding layer
G2 = G.copy()
G2.remove_nodes_from([n for n in list(G2) if n.startswith("P:") and persons[n[2:]]["tiers"] == {4}])
G2.remove_node("O:promt") if "O:promt" in G2 else None
l2 = max(nx.connected_components(G2), key=len); G2 = G2.subgraph(l2).copy()
b2 = nx.betweenness_centrality(G2, normalized=True)
res["top_btw_persons_no_tier4"] = sorted(((n[2:], round(b2[n], 4)) for n in G2 if n.startswith("P:")), key=lambda x: -x[1])[:10]
res["top_btw_orgs_no_tier4"] = sorted(((n[2:], round(b2[n], 4)) for n in G2 if n.startswith("O:")), key=lambda x: -x[1])[:10]

# articulation points (cut vertices)
art = set(nx.articulation_points(G))
res["person_cut_vertices"] = sorted(n[2:] for n in art if n.startswith("P:"))
res["org_cut_vertices"] = sorted(n[2:] for n in art if n.startswith("O:"))

# party assortativity on person projection (unweighted), several variants
def party_assort(exclude_orgs=(), tiers=None):
    H = nx.Graph()
    for t in ties:
        if t["org"] in exclude_orgs: continue
        if tiers and t["tier"] not in tiers: continue
        H.add_node("P:"+t["henkilo"], party=persons[t["henkilo"]]["party"])
        H.add_edge("P:"+t["henkilo"], "O:"+t["org"])
    pn = [n for n in H if n.startswith("P:")]
    if not pn: return None
    PP = bipartite.projected_graph(H, pn)
    PP.remove_nodes_from([n for n in list(PP) if PP.degree(n) == 0])
    if PP.number_of_edges() == 0: return None
    r = nx.attribute_assortativity_coefficient(PP, "party")
    parts = collections.defaultdict(set)
    for n in PP: parts[PP.nodes[n]["party"]].add(n)
    Q = nx.algorithms.community.modularity(PP, parts.values())
    return {"nodes": PP.number_of_nodes(), "edges": PP.number_of_edges(), "r": round(r, 3), "Q_party": round(Q, 3)}
res["assort_all"] = party_assort()
res["assort_no_promt"] = party_assort(exclude_orgs=("promt",))
res["assort_no_promt_kela"] = party_assort(exclude_orgs=("promt", "kela"))
res["assort_tier12"] = party_assort(tiers={1, 2})
res["assort_tier123"] = party_assort(tiers={1, 2, 3})

# Louvain on full graph
comms = nx.algorithms.community.louvain_communities(G, seed=7, resolution=1.0)
res["louvain_Q"] = round(nx.algorithms.community.modularity(G, comms), 3)
cs = []
for c in sorted(comms, key=len, reverse=True):
    orgs = [n[2:] for n in c if n.startswith("O:")]
    ps = [n[2:] for n in c if n.startswith("P:")]
    cs.append({"size": len(c), "orgs": orgs, "n_persons": len(ps),
               "parties": dict(collections.Counter(persons[x]["party"] for x in ps)),
               "haarat": dict(collections.Counter(HAARA[o] for o in orgs))})
res["louvain"] = cs
# haara partition modularity vs party partition on full graph (orgs by haara; persons by their dominant org haara)
def dom_haara(n):
    hs = collections.Counter(HAARA[o[2:]] for o in GB[n]) if n in GB else collections.Counter()
    return hs.most_common(1)[0][0]
part_h = collections.defaultdict(set)
for n in G:
    h = HAARA[n[2:]] if n.startswith("O:") else dom_haara(n)
    part_h[h].add(n)
res["Q_haara_full"] = round(nx.algorithms.community.modularity(G, part_h.values()), 3)

# targeted removal (Newman 15.3): LCC fraction of org nodes
def lcc_orgs(H):
    if len(H) == 0: return 0
    best = max(nx.connected_components(H), key=len)
    return sum(1 for n in best if n.startswith("O:"))
orgs_total = sum(1 for n in G if n.startswith("O:"))
order_p = [n for n, _ in sorted(((n, btw[n]) for n in G if n.startswith("P:")), key=lambda x: -x[1])]
order_o = [n for n, _ in sorted(((n, btw[n]) for n in G if n.startswith("O:")), key=lambda x: -x[1])]
rem = {"persons": [], "orgs": []}
for k in [0, 1, 3, 5, 10, 20, len(order_p)]:
    H = G.copy(); H.remove_nodes_from(order_p[:k]); rem["persons"].append((k, lcc_orgs(H), orgs_total))
for k in [0, 1, 3, 5, 10]:
    H = G.copy(); H.remove_nodes_from(order_o[:k]); rem["orgs"].append((k, lcc_orgs(H), orgs_total, [x[2:] for x in order_o[:k]]))
res["removal"] = rem
# money layer only: are orgs connected without any politicians?
GM = G.copy(); GM.remove_nodes_from([n for n in G if n.startswith("P:")])
res["money_layer_components"] = [sorted(x[2:] for x in c) for c in sorted(nx.connected_components(GM), key=len, reverse=True)]

# clustering
res["avg_clustering_full"] = round(nx.average_clustering(G), 3)
res["density_bipartite"] = round(bipartite.density(GB, P_nodes), 4)

# party coverage: share of each party's persons in tiers 1-3
json.dump(res, open("results.json", "w"), ensure_ascii=False, indent=1, default=lambda o: sorted(o) if isinstance(o, set) else dict(o) if isinstance(o, collections.Counter) else str(o))
print(json.dumps({k: res[k] for k in ["n_persons","n_ties","n_orgs_with_politicians","n_orgs_total","party_conflicts","persons_by_party","persons_by_mintier","ties_by_tier","lcc_size","full_size","kmax","assort_all","assort_no_promt","assort_no_promt_kela","assort_tier12","assort_tier123","louvain_Q","Q_haara_full","avg_clustering_full"]}, ensure_ascii=False, default=lambda o: dict(o) if isinstance(o, collections.Counter) else sorted(o) if isinstance(o,set) else str(o), indent=1))
