"""Check whether sub-case (ii.B) of Case (b) of the Flank-covering
lemma (n_i = 6) ever arises in chord-apex+Kempe colourings.

Sub-case (ii.B) is the configuration in which the structural
propagation argument in the partial proof has a real gap:

  - n_i = 6: F_flank_{i, i+1}^♭ has 2 intermediates P_1, P_2 on the
    F-arc from A_i to A_{i+1}.
  - Case (b): varphi(A_i P_1) = c_1.
  - Sub-case (ii): varphi(P_1 P_2) = c_0.
  - Derived: varphi(A_{i+1} P_2) = c_1, varphi(P_2 O_2) = c,
    varphi(P_1 O_1) = c, varphi(A_i Q) = c.

In this sub-case the lemma's local argument (that the cycle at P_2
passes from P_2 to P_1) needs P_2 ∈ V(K_b), which isn't forced from
the local data: the {c, c_0}-Kempe cycle through P_2 might not be K_b.

We check: across all chord-apex+Kempe colourings of all reduced duals
with |V(G)| ≤ 20, does sub-case (ii.B) ever occur? If yes, in how many
of those configurations is P_1 actually in V(K_b) ∪ V(K_c) (i.e., the
conclusion of the lemma still holds even though our proof gap is
real)?

Run with: sage experiments/check_subcase_iib.py
"""
import os
import sys
import time

from sage.all import Graph
from sage.graphs.graph_generators import graphs

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

from check_conj_final_scaled import (
    apply_reduction,
    proper_3_edge_colorings,
    matches_chord_apex_kempe,
    kempe_cycle_set,
    edge_idx,
)
from check_heawood_on_kempe import dual_of, vertices_of_kempe


def find_flank_arc_intermediates(H, named, i, side):
    """Return the ordered F-arc from A_i (or A_{i+1}) along F's
    boundary as a list of vertices.

    side='lower': arc from A_i to A_{i+1} (= F_flank_{i, i+1}^♭).
    side='upper': arc from A_{i+1} to A_{i+2} (= F_flank_{i+1, i+2}^♭).

    We use the planar embedding of H to walk around the
    face containing v_n + side_0 + spike (lower) or v_n + spike + side_1
    (upper).
    """
    H.is_planar(set_embedding=True)
    spike = named['spike']  # frozenset({A_{i+1}, v_n})
    side_0 = named['side_0']
    side_1 = named['side_1']
    # The flank face contains v_n + spike + side_0/side_1 + the arc edges
    target_edges = set()
    if side == 'lower':
        target_edges = {side_0, spike}
    else:
        target_edges = {side_1, spike}
    for face in H.faces():
        # face is list of (u, v) edges (or tuples of vertex pairs)
        face_edges_set = {frozenset(e) for e in face}
        if target_edges.issubset(face_edges_set):
            # Trace the boundary, return the vertices in cyclic order
            verts = []
            for e in face:
                verts.append(e[0])
            return verts
    return None


def test_one(D):
    D.is_planar(set_embedding=True)
    n_col = 0
    n_subcase_iib = 0
    n_subcase_iib_p1_covered = 0  # of those, how many have P_1 ∈ V(K_b) ∪ V(K_c)
    for face in D.faces():
        if len(face) != 5: continue
        for i_red in range(5):
            res = apply_reduction(D, face, i_red, 9999)
            if res is None: continue
            H = res['H']; named = res['named']
            H.is_planar(set_embedding=True)
            edges, colorings = proper_3_edge_colorings(H)
            cand = [c for c in colorings
                    if matches_chord_apex_kempe(edges, c, named)]
            for col in cand:
                n_col += 1
                # We need to identify P_1, P_2, A_i, A_{i+1}, etc.
                # Recover the named vertices from `named`.
                v_n = 9999
                # named['side_0'] = {A_i, v_n}, named['spike'] = {A_{i+1}, v_n}
                # side_1 = {A_{i+2}, v_n}, merged = {A_{i+3}, A_{i+4}}
                A_i = next(iter(named['side_0'] - {v_n}))
                A_ip1 = next(iter(named['spike'] - {v_n}))
                A_ip2 = next(iter(named['side_1'] - {v_n}))
                A_ip3, A_ip4 = sorted(named['merged'])
                # Get flank arc for lower (A_i → A_{i+1})
                # The face F_flank_{i, i+1}^♭ has boundary v_n - A_i - ... - A_{i+1} - v_n
                # Find this face:
                target_edges = {named['side_0'], named['spike']}
                arc_verts = None
                for f in H.faces():
                    f_edges = {frozenset(e) for e in f}
                    if target_edges.issubset(f_edges):
                        arc_verts = [e[0] for e in f]
                        break
                if arc_verts is None or len(arc_verts) != 5:
                    # Not the n_i = 6 case (length 5 flank face)
                    continue
                # Identify P_1, P_2 along the arc
                # The cycle visits v_n, A_i, P_1, P_2, A_{i+1} in some order
                # (possibly reversed). Find the index of v_n.
                if v_n not in arc_verts: continue
                k = arc_verts.index(v_n)
                cyc = arc_verts[k:] + arc_verts[:k]   # rotate so v_n is first
                # cyc should be [v_n, A_i, P_1, P_2, A_{i+1}] or
                # [v_n, A_{i+1}, P_2, P_1, A_i]
                if cyc[1] == A_i:
                    P_1, P_2 = cyc[2], cyc[3]
                    assert cyc[4] == A_ip1
                elif cyc[1] == A_ip1:
                    P_2, P_1 = cyc[2], cyc[3]
                    assert cyc[4] == A_i
                else:
                    continue
                # Check sub-case (ii.B): φ(A_i P_1) = c_1, φ(P_1 P_2) = c_0
                # c = color of merged/spike
                merged_idx = edge_idx(edges, named['merged'])
                c = col[merged_idx]
                # side_0 has color c_0
                side_0_idx = edge_idx(edges, named['side_0'])
                c_0 = col[side_0_idx]
                # side_1 has color c_1
                side_1_idx = edge_idx(edges, named['side_1'])
                c_1 = col[side_1_idx]
                # Get edge colours of (A_i, P_1) and (P_1, P_2)
                e_AiP1 = edge_idx(edges, frozenset((A_i, P_1)))
                e_P1P2 = edge_idx(edges, frozenset((P_1, P_2)))
                if e_AiP1 is None or e_P1P2 is None:
                    continue
                if col[e_AiP1] != c_1 or col[e_P1P2] != c_0:
                    continue
                # Sub-case (ii.B) detected
                n_subcase_iib += 1
                # Check if P_1 ∈ V(K_b) ∪ V(K_c)
                a = c
                other = [x for x in range(3) if x != a]
                # K_b = {a, b_color} Kempe cycle through merged
                kc_b = kempe_cycle_set(edges, col, merged_idx, (a, other[0]))
                kc_c = kempe_cycle_set(edges, col, merged_idx, (a, other[1]))
                V_b = vertices_of_kempe(edges, kc_b)
                V_c = vertices_of_kempe(edges, kc_c)
                if P_1 in V_b or P_1 in V_c:
                    n_subcase_iib_p1_covered += 1
    return n_col, n_subcase_iib, n_subcase_iib_p1_covered


def main(max_n=20, time_budget_per_n=1800):
    print("Check: how often does sub-case (ii.B) of Case (b) of\n"
          "Lemma 'Flank covering, n_i = 6' actually arise?\n"
          f"n_G in [12, {max_n}]\n")
    grand_col = 0
    grand_sub = 0
    grand_sub_covered = 0
    for n in range(12, max_n + 1):
        start = time.time()
        try:
            triangulations = list(graphs.triangulations(n, minimum_degree=5))
        except Exception as ex:
            print(f"n={n}: cannot enumerate ({ex})")
            continue
        n_col_n = 0; n_sub_n = 0; n_cov_n = 0
        for tri_idx, G in enumerate(triangulations):
            if time.time() - start > time_budget_per_n:
                print(f"  n={n}: timeout at tri {tri_idx}/{len(triangulations)}")
                break
            G.is_planar(set_embedding=True)
            D = dual_of(G)
            ni, ns, ncov = test_one(D)
            n_col_n += ni; n_sub_n += ns; n_cov_n += ncov
        elapsed = time.time() - start
        print(f"n={n}: {n_col_n} col., {n_sub_n} sub-case (ii.B) "
              f"({100*n_sub_n/max(n_col_n, 1):.2f}% of col.); "
              f"{n_cov_n} of those have P_1 ∈ V(K_b)∪V(K_c) "
              f"[{elapsed:.0f}s]")
        sys.stdout.flush()
        grand_col += n_col_n
        grand_sub += n_sub_n
        grand_sub_covered += n_cov_n
    print()
    print("=" * 70)
    print(f"Grand totals: {grand_col} chord-apex+Kempe colourings")
    print(f"  sub-case (ii.B) detected: {grand_sub} "
          f"({100*grand_sub/max(grand_col, 1):.2f}%)")
    if grand_sub > 0:
        print(f"  of those, P_1 ∈ V(K_b)∪V(K_c): {grand_sub_covered} "
              f"({100*grand_sub_covered/max(grand_sub, 1):.2f}%)")
        gap_open = grand_sub - grand_sub_covered
        print(f"  of those, P_1 NOT in V(K_b)∪V(K_c): {gap_open}")
    else:
        print("  Sub-case (ii.B) never arises empirically.")
        print("  → the proof gap is structurally INACTIVE")
        print("  → the n_i = 6 lemma's conclusion is empirically tight")


if __name__ == '__main__':
    main()
