"""
Double counting at the face/source level.
For each (G_iso, source-up-to-aut, flip choice), apply the resolution and
track which G'_iso it lands on. Then aggregate.

For each md4 target G' we report:
- N_iso(G')   = # distinct preimage iso-classes G that resolve to G'
- N_paths(G') = # distinct (G_iso, source orbit, flip choice) triples

For each source G we report:
- f(G) = # (source, flip choice) pairs producing any md4 target
"""
import sys; sys.path.insert(0, '/home/claude/build')
import networkx as nx
from collections import defaultdict
from itertools import product as iproduct

from triangulation_gen import enumerate_all_triangulations
from triangulation_gen_fast import enumerate_all_triangulations_fast
from level_cycles import (
    compute_levels, get_all_faces, get_level_cycles_by_parity,
    get_flip_candidates, level_sources,
)


def min_degree(G):
    return min(d for _, d in G.degree())


def iso_class(G, reps):
    for i, r in enumerate(reps):
        if nx.is_isomorphic(G, r):
            return i
    return -1


def all_resolutions(G):
    """Yield (G_resolved, source_kind, source_label) for every resolution.
    Equivalent paths are NOT deduplicated here — caller can do so via iso."""
    ip, emb = nx.check_planarity(G)
    if not ip: return
    for kind, label, source_set in level_sources(G, emb):
        levels = compute_levels(G, source_set)
        odd_cycles, even_cycles = get_level_cycles_by_parity(G, emb, levels)
        cand_lists = []
        ok = True
        for cycle in odd_cycles:
            cands = get_flip_candidates(G, emb, cycle)
            if not cands: ok = False; break
            cand_lists.append(cands)
        if not ok: continue
        for cycle in even_cycles:
            cands = get_flip_candidates(G, emb, cycle)
            cand_lists.append(cands + [None])
        if not cand_lists:
            yield (G.copy(), kind, label); continue
        for choices in iproduct(*cand_lists):
            Gp = G.copy()
            applied = set(); ok2 = True
            for c in choices:
                if c is None: continue
                u, v, w, x = c
                if frozenset([u,v]) in applied: ok2 = False; break
                if not Gp.has_edge(u, v) or Gp.has_edge(w, x): ok2 = False; break
                Gp.remove_edge(u, v); Gp.add_edge(w, x)
                applied.add(frozenset([u, v]))
            if not ok2: continue
            ipp, _ = nx.check_planarity(Gp)
            if not ipp: continue
            yield (Gp, kind, label)


def analyze(n, use_fast=False):
    print(f"\n{'='*70}\nn = {n}\n{'='*70}")
    if use_fast:
        reps = enumerate_all_triangulations_fast(n)
    else:
        reps = enumerate_all_triangulations(n)
    md4_idx = [i for i, G in enumerate(reps) if min_degree(G) >= 4]
    md4_set = set(md4_idx)

    # For each source iso-class, compute # paths to each target iso-class
    pre_count = defaultdict(lambda: defaultdict(int))   # target -> source -> count
    src_paths_md4 = defaultdict(int)  # source -> total paths landing on md4

    print(f"Computing resolutions for {len(reps)} sources...", flush=True)
    for src_i, G in enumerate(reps):
        # Compute all distinct (target iso, source orbit) pairs from this G
        for Gp, kind, label in all_resolutions(G):
            tgt = iso_class(Gp, reps)
            if tgt < 0: continue
            pre_count[tgt][src_i] += 1
            if tgt in md4_set:
                src_paths_md4[src_i] += 1

    # Report by md4 target
    print(f"\n{'target':28s} {'N_iso':>6} {'N_paths':>9} {'min path/iso':>14}")
    print("-" * 70)
    for tgt in md4_idx:
        deg = sorted((d for _, d in reps[tgt].degree()), reverse=True)
        sources_hitting = list(pre_count[tgt].keys())
        N_iso = len(sources_hitting)
        N_paths = sum(pre_count[tgt].values())
        min_paths_per_src = (min(pre_count[tgt].values())
                              if sources_hitting else 0)
        print(f"{str(deg):28s} {N_iso:>6} {N_paths:>9} {min_paths_per_src:>14}")
    # Double-counting check:
    total_md4_paths = sum(sum(d.values()) for d in pre_count.values()
                           if any(k in md4_set for k in [...]))
    # Simpler total
    total_paths = sum(src_paths_md4.values())
    avg_per_target = total_paths / len(md4_idx) if md4_idx else 0
    print("-" * 70)
    print(f"Total md4 paths: {total_paths}")
    print(f"Average per md4 target: {avg_per_target:.1f}")
    if md4_idx:
        min_N_iso = min(len(pre_count[t]) for t in md4_idx)
        print(f"Minimum N_iso over md4 targets: {min_N_iso}")

    return pre_count, md4_idx


if __name__ == "__main__":
    for n in [6, 7, 8, 9, 10]:
        analyze(n, use_fast=(n >= 10))
