"""Constrained 3-edge-colouring feasibility on H_t*.

The chord-apex (Lemma 2.6) and Kempe-cycle (Lemma 2.7) lemmas give proven
constraints on proper 3-edge-colourings of H_1 when G is a minimal
counterexample. The step-1 constraints, *interpreted on H_t*, are:

  (C1) phi(spike_1) = phi(merged_1)                                   [chord-apex]
  (K0) the {phi(spike_1), phi(side_0_1)}-Kempe cycle of H_t* through
       spike_1 contains side_0_1 and merged_1
  (K1) the {phi(spike_1), phi(side_1_1)}-Kempe cycle of H_t* through
       spike_1 contains side_1_1 and merged_1

If G is a minimal counterexample then every proper 3-edge-colouring of H_t*
that *lifts back* to a proper 3-edge-colouring of H_1 must satisfy (C1) +
(K0) + (K1) on H_1; the Kempe-cycle structure on H_t* and H_1 can differ,
but we test the H_t*-interpreted versions here as the natural constraint
set the algorithm propagates.

For each min-deg-5 triangulation G and each (face, i_red) we enumerate
proper 3-edge-colourings of H_t* and count those satisfying these step-1
constraints. If the constrained problem is INFEASIBLE on H_t* for some G
in our test set, that would be a hint that constraints alone can rule out
3-edge-colourability of H_t* -- a potential mechanism for proving smaller
counterexamples exist.

Run with: sage experiments/check_constrained_feasibility.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
from sage.graphs.graph_coloring import edge_coloring
import sys


def dual_of(G):
    G.is_planar(set_embedding=True)
    faces = G.faces()
    edge_to_faces = {}
    for fi, face in enumerate(faces):
        for u, v in face:
            edge_to_faces.setdefault(frozenset((u, v)), []).append(fi)
    return Graph(
        [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2],
        multiedges=False, loops=False)


def proper_3_edge_colorings(G):
    edges = list(G.edges(labels=False))
    n = len(edges)
    adj = [[] for _ in range(n)]
    for i in range(n):
        u, v = edges[i][0], edges[i][1]
        for j in range(i):
            x, y = edges[j][0], edges[j][1]
            if u in (x, y) or v in (x, y):
                adj[i].append(j); adj[j].append(i)
    coloring = [-1] * n
    results = []

    def back(k):
        if k == n:
            results.append(tuple(coloring))
            return
        for c in range(3):
            if all(coloring[j] != c for j in adj[k]):
                coloring[k] = c
                back(k + 1)
        coloring[k] = -1
    back(0)
    return edges, results


def kempe_cycle(edges, coloring, start_idx, color_pair):
    a, b = color_pair
    if coloring[start_idx] not in (a, b):
        return set()
    in_sub = set(i for i in range(len(edges)) if coloring[i] in (a, b))
    visited = {start_idx}
    stack = [start_idx]
    while stack:
        cur = stack.pop()
        u, v = edges[cur][0], edges[cur][1]
        for j in in_sub:
            if j in visited:
                continue
            x, y = edges[j][0], edges[j][1]
            if u in (x, y) or v in (x, y):
                visited.add(j); stack.append(j)
    return visited


def edge_idx(edges, e_frozen):
    for i, e in enumerate(edges):
        if frozenset((e[0], e[1])) == e_frozen:
            return i
    return None


def apply_reduction(G, face, i, v_n_label):
    boundary = [u for (u, v) in face]
    if len(set(boundary)) != 5: return None
    A = []
    for B_k in boundary:
        outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
        if len(outer) != 1: return None
        A.append(outer[0])
    if len(set(A)) != 5 or A[(i+3) % 5] == A[(i+4) % 5]: return None
    H = G.copy()
    for v in boundary: H.delete_vertex(v)
    H.add_vertex(v_n_label)
    side_0 = (v_n_label, A[i])
    spike  = (v_n_label, A[(i+1) % 5])
    side_1 = (v_n_label, A[(i+2) % 5])
    merged = (A[(i+3) % 5], A[(i+4) % 5])
    H.add_edges([side_0, spike, side_1, merged])
    if H.has_multiple_edges() or H.has_loops(): return None
    if not H.is_planar(set_embedding=True): return None
    if not all(H.degree(v) == 3 for v in H.vertex_iterator()): return None
    return {
        'H': H,
        'named': {
            'spike':  frozenset(spike), 'side_0': frozenset(side_0),
            'side_1': frozenset(side_1), 'merged': frozenset(merged),
        },
    }


def find_safe_pentagonal_face(G, protected):
    for face in G.faces():
        if len(face) != 5: continue
        boundary = [u for (u, v) in face]
        boundary_edges = [frozenset([u, v]) for (u, v) in face]
        externals, A, ok = [], [], True
        for B_k in boundary:
            outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
            if len(outer) != 1: ok = False; break
            externals.append(frozenset([B_k, outer[0]])); A.append(outer[0])
        if not ok: continue
        if any(e in protected for e in boundary_edges + externals): continue
        return boundary, externals, A
    return None


def valid_index(f_vec, A):
    for i in range(5):
        if f_vec[(i+3) % 5] != f_vec[(i+4) % 5]: continue
        if len({f_vec[i], f_vec[(i+1) % 5], f_vec[(i+2) % 5]}) != 3: continue
        if A[(i+3) % 5] == A[(i+4) % 5]: continue
        return i
    return None


def run_to_completion(H, phi_dict, named_step1, vn_start=10000):
    H = H.copy()
    coloring = dict(phi_dict)
    all_named = [named_step1]
    E = set(named_step1.values())
    next_vn = vn_start
    while True:
        H.is_planar(set_embedding=True)
        result = find_safe_pentagonal_face(H, E)
        if result is None: break
        boundary, externals, A = result
        f_vec = [coloring[e] for e in externals]
        i_t = valid_index(f_vec, A)
        if i_t is None: break
        v_n = next_vn; next_vn += 1
        H_new = H.copy()
        for v in boundary: H_new.delete_vertex(v)
        H_new.add_vertex(v_n)
        s0 = (v_n, A[i_t]); sp = (v_n, A[(i_t+1) % 5])
        s1 = (v_n, A[(i_t+2) % 5]); mg = (A[(i_t+3) % 5], A[(i_t+4) % 5])
        H_new.add_edges([s0, sp, s1, mg])
        if H_new.has_multiple_edges() or H_new.has_loops(): break
        if not H_new.is_planar(set_embedding=True): break
        H = H_new
        coloring = {e: c for e, c in coloring.items()
                    if not any(u in boundary for u in e)}
        coloring[frozenset(s0)] = f_vec[i_t]
        coloring[frozenset(sp)] = f_vec[(i_t+1) % 5]
        coloring[frozenset(s1)] = f_vec[(i_t+2) % 5]
        coloring[frozenset(mg)] = f_vec[(i_t+3) % 5]
        all_named.append({
            'spike':  frozenset(sp), 'side_0': frozenset(s0),
            'side_1': frozenset(s1), 'merged': frozenset(mg),
        })
        E |= set(all_named[-1].values())
    return H, coloring, all_named


def check_constraints(edges, col, named):
    """Check (C1) + (K0) + (K1) on this colouring."""
    idx = {role: edge_idx(edges, ns) for role, ns in named.items()}
    if any(v is None for v in idx.values()):
        return False, False, False
    c_spike  = col[idx['spike']]
    c_merged = col[idx['merged']]
    C1 = (c_spike == c_merged)
    if not C1:
        return False, False, False
    c_s0 = col[idx['side_0']]; c_s1 = col[idx['side_1']]
    kc0 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s0))
    kc1 = kempe_cycle(edges, col, idx['spike'], (c_spike, c_s1))
    K0 = idx['side_0'] in kc0 and idx['merged'] in kc0
    K1 = idx['side_1'] in kc1 and idx['merged'] in kc1
    return C1, K0, K1


def run_case(G, n, tri_idx):
    G.is_planar(set_embedding=True)
    D = dual_of(G); D.is_planar(set_embedding=True)
    for face_n, face in enumerate([f for f in D.faces() if len(f) == 5]):
        for i_red in range(5):
            res = apply_reduction(D, face, i_red, 9999)
            if res is None: continue
            H_1, named_1 = res['H'], res['named']
            # Default phi_1: Sage's edge_coloring
            try:
                classes = edge_coloring(H_1, value_only=False)
            except Exception:
                continue
            if len(classes) > 3: continue
            phi_1_dict = {}
            for k, edge_list in enumerate(classes):
                for u, v in edge_list:
                    phi_1_dict[frozenset([u, v])] = k
            H_final, _, all_named = run_to_completion(H_1, phi_1_dict, named_1)
            edges_f, colorings_f = proper_3_edge_colorings(H_final)
            n_total = len(colorings_f)
            n_c1 = 0; n_c1_k0 = 0; n_c1_k1 = 0; n_all = 0
            for col in colorings_f:
                C1, K0, K1 = check_constraints(edges_f, col, named_1)
                if C1: n_c1 += 1
                if C1 and K0: n_c1_k0 += 1
                if C1 and K1: n_c1_k1 += 1
                if C1 and K0 and K1: n_all += 1
            feasible = n_all > 0
            print(f"  n={n} tri#{tri_idx} face#{face_n} i_red={i_red}: "
                  f"|V(H_t*)|={H_final.order()}, |E|={H_final.size()}, "
                  f"colorings={n_total}, "
                  f"C1={n_c1}, C1+K0={n_c1_k0}, C1+K1={n_c1_k1}, "
                  f"C1+K0+K1={n_all} "
                  f"{'OK' if feasible else '*** INFEASIBLE ***'}")
            sys.stdout.flush()
            return  # one (face, i_red) per triangulation


def main(max_n=15):
    for n in range(12, max_n + 1):
        print(f"\n=== n = {n} ===")
        try:
            triangulations = list(graphs.triangulations(n, minimum_degree=5))
        except Exception as ex:
            print(f"  cannot enumerate: {ex}"); continue
        for tri_idx, G in enumerate(triangulations, start=1):
            run_case(G, n, tri_idx)


if __name__ == '__main__':
    main()
