"""
Extended orbit-counting framework: REVERSE-FLIPS ON ANY SET OF EDGES.

For each md4 target G' and each k ∈ {1, 2, ...}:
- Enumerate distinct (up to iso) k-flip reverse-preimages G of G'
- For each, check if G level-resolves to G' via any source

Surjectivity at G' = some k and some preimage works.

Includes a critical test: the icosahedron at n=12 (no deg-4 vertices).
"""
import sys; sys.path.insert(0, '/home/claude/build')
import networkx as nx
import time
from collections import defaultdict
from itertools import combinations, product as iproduct

from level_cycles import (
    compute_levels, get_all_faces, get_level_cycles_by_parity,
    get_flip_candidates, level_sources,
)


def reverse_flip_edge(G_prime, e):
    u, v = tuple(e)
    if not G_prime.has_edge(u, v): return None
    _, emb = nx.check_planarity(G_prime)
    f1 = emb.traverse_face(u, v); f2 = emb.traverse_face(v, u)
    if len(f1) != 3 or len(f2) != 3: return None
    a = next(x for x in f1 if x != u and x != v)
    b = next(x for x in f2 if x != u and x != v)
    if a == b or G_prime.has_edge(a, b): return None
    G = G_prime.copy()
    G.remove_edge(u, v); G.add_edge(a, b)
    ip, _ = nx.check_planarity(G)
    if not ip: return None
    return G


def k_flip_preimages_iso(G_prime, k):
    """Yield distinct (up to iso) triangulations obtainable from G_prime by
    k successive reverse-flips."""
    seen = [G_prime]
    frontier = [G_prime]
    for step in range(k):
        new_frontier = []
        new_seen = []
        for H in frontier:
            for e in list(H.edges()):
                Hp = reverse_flip_edge(H, frozenset(e))
                if Hp is None: continue
                if any(nx.is_isomorphic(Hp, S) for S in seen + new_seen):
                    continue
                new_seen.append(Hp)
                new_frontier.append(Hp)
        seen.extend(new_seen)
        frontier = new_frontier
        if not frontier: break
    return frontier  # candidates at depth EXACTLY k


def resolves_to_target(G, target):
    """Does G level-resolve to iso(target) via any source?"""
    ip, emb = nx.check_planarity(G)
    if not ip: return False
    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:
            if nx.is_isomorphic(G, target): return True
            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
            if nx.is_isomorphic(Gp, target): return True
    return False


def check_target(G_prime, k_max=3, label=""):
    """For target G_prime, scan k=1, 2, ..., k_max for a preimage that works.
    Return (success_k, preimage_idx) or (None, None) if all fail."""
    for k in range(1, k_max + 1):
        t0 = time.time()
        cands = k_flip_preimages_iso(G_prime, k)
        n_cand = len(cands)
        for i, G in enumerate(cands):
            if resolves_to_target(G, G_prime):
                elapsed = time.time() - t0
                print(f"  {label}: k={k}, hit at {i+1}/{n_cand} "
                      f"({elapsed:.1f}s)")
                return k, i
        elapsed = time.time() - t0
        print(f"  {label}: k={k}, no hits among {n_cand} preimages "
              f"({elapsed:.1f}s)")
    return None, None


if __name__ == "__main__":
    # Test on icosahedron
    G_ico = nx.icosahedral_graph()
    print("=" * 60)
    print("Icosahedron (n=12, 5-regular)")
    print("=" * 60)
    k, idx = check_target(G_ico, k_max=3, label="icosahedron")
    print(f"Result: reachable at k={k}" if k else "NOT reachable up to k=3")
