"""Hand-check scaffold for the inductive lift step (route 1).

For each md4 iso-class T at n:
  1. Pick a degree-4 vertex v.
  2. Contract v: delete v and retriangulate the resulting 4-cycle with one
     of two possible diagonals. This gives T' on n-1 vertices.
  3. Identify T's iso-class at n-1.
  4. Find empirical (G', S') such that the algorithm produces T'.
  5. Lift G' to G on n vertices by reinserting v in a corresponding face.
  6. Run the algorithm on (G, S) and check if it produces T.

Reports what worked / failed for each pair (md4 target, degree-4 vertex).
"""
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, iso_index, is_self_level_resolution,
)


def cyclic_neighbors(G, v):
    """Return the cyclic order of v's neighbors in G's planar embedding."""
    ip, emb = nx.check_planarity(G)
    if not ip:
        return None
    return list(emb.neighbors_cw_order(v))


def contract_degree4_vertex(G, v):
    """Remove a degree-4 vertex v from G and retriangulate the 4-cycle
    formed by its neighbors. Yield (T', diagonal) for each of the two
    possible diagonal choices."""
    cyc = cyclic_neighbors(G, v)
    if cyc is None or len(cyc) != 4:
        return
    a, b, c, d = cyc
    for (u, w) in [(a, c), (b, d)]:
        Gp = G.copy()
        Gp.remove_node(v)
        if Gp.has_edge(u, w):
            continue
        Gp.add_edge(u, w)
        ip, _ = nx.check_planarity(Gp)
        if not ip:
            continue
        if Gp.number_of_edges() != 3 * (Gp.number_of_nodes()) - 6:
            continue
        yield Gp, (u, w), (a, b, c, d)


def find_labeled_preimage(target_labeled, isos):
    """Find any (G, S) at n vertices such that algorithm yields a graph
    EXACTLY equal (as labeled simple graph) to target_labeled.
    To get all possible labelings, we search over the iso-class enumeration
    plus all relabeling isomorphisms.
    Returns (G, source_set, Gp_out) or None."""
    target_edges = set(frozenset(e) for e in target_labeled.edges())
    target_nodes = set(target_labeled.nodes())
    n = len(target_nodes)
    for G in isos:
        # Try all label mappings from G's vertices to target_labeled's vertices.
        # For small n this is feasible; otherwise we'd use isomorphism enum.
        gm = nx.isomorphism.GraphMatcher(G, target_labeled)  # not what we want
        # We want to find an iso from G to *some* labeled triangulation
        # H, where algorithm(H, S) = target_labeled exactly.
        # Easier: just enumerate label permutations of G's vertices and
        # check each.
        from itertools import permutations
        # Use permutations only if n is small (n<=7 is fine, n=8 = 40k perms)
        if n > 7:
            continue
        for perm in permutations(target_nodes, n):
            relabel = {orig: perm[i] for i, orig in enumerate(sorted(G.nodes()))}
            H = nx.relabel_nodes(G, relabel, copy=True)
            ip, emb = nx.check_planarity(H)
            if not ip:
                continue
            for kind, lab, source_set in level_sources(H, emb):
                for Hp in simple_level_resolutions(H, source_set):
                    if not is_triangulation(Hp, n):
                        continue
                    Hp_edges = set(frozenset(e) for e in Hp.edges())
                    if Hp_edges == target_edges:
                        return (H, source_set, Hp)
    return None


def insert_vertex_into_face(G, face_vertices, new_v):
    """Insert new_v into the triangular face of G defined by face_vertices.
    Returns the new graph. The face must currently be a triangle."""
    Gp = G.copy()
    Gp.add_node(new_v)
    for u in face_vertices:
        Gp.add_edge(new_v, u)
    return Gp


def is_md4(T):
    return min(T.degree(v) for v in T.nodes()) >= 4


def attempt_lift(G_prime, target_T, cyc, diag, v_label):
    """Attempt to lift G' to G on n vertices by reinserting v.

    Idea: if the diagonal (u, w) is in G_prime and (u, w) borders two
    triangles in G_prime, we can "uncontract" by removing the diagonal
    and inserting v adjacent to the 4 boundary vertices of those two
    triangles. The 4 boundary vertices should match cyc up to rotation
    + reflection.

    Returns G (the lifted graph) if successful, else None.
    """
    u, w = diag
    if not G_prime.has_edge(u, w):
        return None, "diagonal not in G'"
    ip, emb = nx.check_planarity(G_prime)
    if not ip:
        return None, "G' not planar"
    f1 = emb.traverse_face(u, w)
    f2 = emb.traverse_face(w, u)
    if len(f1) != 3 or len(f2) != 3:
        return None, "diagonal not bordered by two triangles"
    x = next(p for p in f1 if p != u and p != w)
    y = next(p for p in f2 if p != u and p != w)
    boundary = {u, w, x, y}
    if boundary != set(cyc):
        return None, f"boundary {boundary} != cyc {set(cyc)}"
    G = G_prime.copy()
    G.remove_edge(u, w)
    G.add_node(v_label)
    for vert in boundary:
        G.add_edge(v_label, vert)
    if G.number_of_edges() != 3 * G.number_of_nodes() - 6:
        return None, f"wrong edge count {G.number_of_edges()}"
    ip2, _ = nx.check_planarity(G)
    if not ip2:
        return None, "lifted G not planar"
    return G, "ok"


def main():
    n = 7
    isos_n = enumerate_all_triangulations(n)
    isos_n1 = enumerate_all_triangulations(n - 1)

    md4_iso = [i for i, T in enumerate(isos_n) if is_md4(T)]
    md4_iso_n1 = [i for i, T in enumerate(isos_n1) if is_md4(T)]
    print(f"n={n}: md4 iso-classes = {md4_iso}")
    print(f"n={n-1}: md4 iso-classes = {md4_iso_n1}")

    for tgt_idx in md4_iso:
        T = isos_n[tgt_idx]
        print(f"\n=== Target T = iso[{tgt_idx}] at n={n} ===")
        deg = {v: T.degree(v) for v in T.nodes()}
        deg4_vertices = [v for v, d in deg.items() if d == 4]

        good_contractions = []
        for v in deg4_vertices:
            for Tp, diag, cyc in contract_degree4_vertex(T, v):
                tp_idx = iso_index(Tp, isos_n1)
                if tp_idx is None:
                    continue
                is_good = tp_idx in md4_iso_n1
                marker = "GOOD (md4)" if is_good else "non-md4"
                print(f"  v={v} diag={diag} cyc={cyc}: T' = iso[{tp_idx}] {marker}")
                if is_good:
                    good_contractions.append((v, diag, cyc, Tp, tp_idx))

        if not good_contractions:
            print(f"  ! NO md4-preserving contraction found")
            continue

        v, diag, cyc, Tp, tp_idx = good_contractions[0]
        print(f"\n  Trying lift via good contraction: v={v} diag={diag}")
        print(f"    T' (labeled, vertices {sorted(Tp.nodes())}): "
              f"edges {sorted(Tp.edges())}")
        # Search for a LABELED preimage of T'.
        labeled_pre = find_labeled_preimage(Tp, isos_n1)
        if labeled_pre is None:
            print(f"    no labeled preimage found for T'")
            continue
        H, source_set, Hp = labeled_pre
        print(f"    labeled preimage H: edges {sorted(H.edges())}")
        print(f"    source: {sorted(source_set)}")
        print(f"    algorithm(H, S) yields T' exactly (labels match)")
        # Now lift H to G by uncontracting at the diagonal
        G_lifted, status = attempt_lift(H, T, cyc, diag, v)
        print(f"    lift status: {status}")
        if G_lifted is None:
            return
        print(f"    lifted G edges: {sorted(G_lifted.edges())}")
        # Run algorithm on G_lifted with the same source set
        success = False
        for Gp_run in simple_level_resolutions(G_lifted, source_set):
            if not is_triangulation(Gp_run, n):
                continue
            if nx.is_isomorphic(Gp_run, T):
                # Check exact label match too
                if set(frozenset(e) for e in Gp_run.edges()) == \
                   set(frozenset(e) for e in T.edges()):
                    print(f"    SUCCESS (label-exact): algorithm yields T")
                else:
                    print(f"    SUCCESS (iso, not label-exact): algorithm "
                          f"yields a graph iso to T")
                success = True
                break
        if not success:
            print(f"    lift OK but algorithm on (G, S) does not yield T")


if __name__ == "__main__":
    main()
