"""Test the user's revised conjecture:

For T = algorithm(G, S), there exists a cross-level edge Z in T
(other than the edges introduced by the algorithm) such that flipping Z
in T gives a graph isomorphic to G.

For various (G, S, T) triples, enumerate cross-level edges of T,
flip each, and check whether the result is iso to G.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import networkx as nx
from collections import defaultdict
from triangulation_gen import enumerate_all_triangulations
from level_cycles import compute_levels, level_sources
from simple_level_resolution_coverage import (
    simple_level_resolutions, is_triangulation,
)


def flip_edge(G, u, v):
    """Flip edge (u, v) in G. Return flipped graph or None if invalid."""
    if not G.has_edge(u, v):
        return None
    ip, emb = nx.check_planarity(G)
    if not ip:
        return None
    f1 = emb.traverse_face(u, v)
    f2 = emb.traverse_face(v, u)
    if len(f1) != 3 or len(f2) != 3:
        return None
    w = next(x for x in f1 if x != u and x != v)
    x = next(y for y in f2 if y != u and y != v)
    if w == x or G.has_edge(w, x):
        return None
    Gp = G.copy()
    Gp.remove_edge(u, v)
    Gp.add_edge(w, x)
    return Gp


def apply_flips(G, edges_to_flip):
    """Apply a sequence of edge flips. Skip flips that are no longer valid
    (e.g., after a previous flip removed the edge)."""
    Gc = G.copy()
    applied = 0
    for (u, v) in edges_to_flip:
        Gp = flip_edge(Gc, u, v)
        if Gp is None:
            continue
        Gc = Gp
        applied += 1
    return Gc, applied


def analyze(G, source_set, T, max_subset=6):
    """For T = algorithm(G, S), enumerate cross-level edges of T (w.r.t.
    G-levels). Try all subsets up to size max_subset of the
    cross-level edges that are INHERITED (i.e., not algorithm-introduced).
    Check if any such subset, when flipped, gives a graph iso to G."""
    from itertools import combinations
    levels = compute_levels(G, source_set)
    G_edges = set(frozenset(e) for e in G.edges())
    T_edges = set(frozenset(e) for e in T.edges())
    algorithm_added = T_edges - G_edges
    cross_in_T = set()
    for e in T_edges:
        u, v = tuple(e)
        if levels.get(u, -1) != levels.get(v, -1):
            cross_in_T.add(e)
    cross_inherited = cross_in_T - algorithm_added
    # Try subsets of cross_inherited from size 1 up to max_subset
    success_subsets = []
    for k in range(1, min(max_subset, len(cross_inherited)) + 1):
        for subset in combinations(cross_inherited, k):
            edge_list = [tuple(e) for e in subset]
            T_flipped, _ = apply_flips(T, edge_list)
            if nx.is_isomorphic(T_flipped, G):
                success_subsets.append(subset)
                break  # first one at this size is enough
        if success_subsets:
            break  # found at smallest size
    return {
        'algorithm_added': len(algorithm_added),
        'cross_inherited': len(cross_inherited),
        'success_subset_size': len(success_subsets[0]) if success_subsets else None,
        'has_iso_recovery': bool(success_subsets),
    }


def run(n, max_per_iso=4):
    isos = enumerate_all_triangulations(n)
    print(f"\n=== n={n} ({len(isos)} iso-classes) ===")
    summaries = []
    for src_idx, G in enumerate(isos):
        ip, emb = nx.check_planarity(G)
        if not ip:
            continue
        for kind, lab, source_set in level_sources(G, emb):
            count = 0
            for Gp in simple_level_resolutions(G, source_set):
                count += 1
                if count > max_per_iso:
                    break
                if not is_triangulation(Gp, n):
                    continue
                # Skip cases with no algorithm flips (G == Gp)
                if set(frozenset(e) for e in Gp.edges()) == \
                   set(frozenset(e) for e in G.edges()):
                    continue
                summary = analyze(G, source_set, Gp)
                summaries.append({
                    'src': src_idx,
                    'source': (kind, lab),
                    **summary,
                })
    print(f"  total (G, S, T) triples (with algorithm flips): {len(summaries)}")
    recoverable = sum(1 for s in summaries if s['has_iso_recovery'])
    print(f"  triples where SOME inherited-cross-level subset recovers iso(G): "
          f"{recoverable} / {len(summaries)}")
    # Distribution of minimal subset size
    size_dist = defaultdict(int)
    for s in summaries:
        size_dist[s['success_subset_size']] += 1
    print(f"  minimal subset size distribution: "
          f"{dict(sorted((k, v) for k, v in size_dist.items() if k is not None))}")
    failures = sum(1 for s in summaries if not s['has_iso_recovery'])
    if failures:
        print(f"  {failures} triples with NO recovery (within max subset size)")


if __name__ == "__main__":
    ns = [int(x) for x in sys.argv[1:]] if len(sys.argv) > 1 else [7, 8]
    for n in ns:
        run(n)
