"""For the 1,314 bad chord-apex+Kempe colourings (with sub-case (ii.B)
+ P_1 ∉ V(K_b) ∪ V(K_c)), check the *face structure* induced by S:

  - Is the S-cycle a face boundary of the reduced dual?
  - For each S-edge, what are the 2 adjacent faces?
  - How many of those F_ext faces are G'-pentagons?
  - Does the bound (# G'-pentagons hit ≤ |S|) hold?

If S being a single cycle is also a face boundary, then # G'-pentagons
hit by S ≤ |S| (one per S-edge, with the F1 interior face being a
|S|-length face = not a pentagon since |S| ≥ 6). Combined with the
p_G ≥ 7 lower bound, this closes |S| ≤ 6 cases.

Run with: sage experiments/check_S_face_structure.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 test_one(D):
    D.is_planar(set_embedding=True)
    bad_count = 0
    # |S| -> # bad colourings of that size where S is a face boundary
    is_face_boundary = {}
    # |S| -> # bad colourings of that size
    by_size = {}
    # |S| -> distribution of (# G'-pentagons hit by S)
    pentagons_hit_by_size = {}
    # |S| -> distribution of (# G'-pentagons total in reduced dual)
    pent_total_by_size = {}

    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)]
            v_n = 9999
            for col in cand:
                # Identify bad sub-case (ii.B)
                target = {named['side_0'], named['spike']}
                lower_flank = None
                for f in H.faces():
                    if target.issubset({frozenset(e) for e in f}):
                        lower_flank = f; break
                if lower_flank is None or len(lower_flank) != 5: continue
                arc_verts = [e[0] for e in lower_flank]
                if v_n not in arc_verts: continue
                k = arc_verts.index(v_n)
                cyc = arc_verts[k:] + arc_verts[:k]
                A_i = next(iter(named['side_0'] - {v_n}))
                A_ip1 = next(iter(named['spike'] - {v_n}))
                if cyc[1] == A_i and cyc[4] == A_ip1:
                    P_1, P_2 = cyc[2], cyc[3]
                elif cyc[1] == A_ip1 and cyc[4] == A_i:
                    P_2, P_1 = cyc[2], cyc[3]
                else: continue
                merged_idx = edge_idx(edges, named['merged'])
                c_col = col[merged_idx]
                c_0_col = col[edge_idx(edges, named['side_0'])]
                c_1_col = col[edge_idx(edges, named['side_1'])]
                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_col or col[e_P1P2] != c_0_col:
                    continue
                a = c_col
                other = [x for x in range(3) if x != a]
                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)
                V_union = V_b | V_c
                S = set(H.vertices()) - V_union
                if P_1 in V_union: continue
                bad_count += 1
                S_size = len(S)
                by_size[S_size] = by_size.get(S_size, 0) + 1

                # Is S a face boundary?
                # Find a face whose boundary vertices = S exactly.
                S_is_face = False
                for f in H.faces():
                    verts = {u for (u, v) in f} | {v for (u, v) in f}
                    if verts == S:
                        S_is_face = True
                        break
                if S_is_face:
                    is_face_boundary[S_size] = (
                        is_face_boundary.get(S_size, 0) + 1)

                # # G'-pentagons (= not adjacent to F_v's modification)
                def is_g_prime_pentagon(f):
                    if len(f) != 5: return False
                    f_edges_set = {frozenset(e) for e in f}
                    if (named['side_0'] in f_edges_set or
                        named['side_1'] in f_edges_set or
                        named['spike'] in f_edges_set or
                        named['merged'] in f_edges_set):
                        return False
                    return True

                p_total = 0
                p_hit = 0
                for f in H.faces():
                    if not is_g_prime_pentagon(f): continue
                    p_total += 1
                    verts = {u for (u, v) in f} | {v for (u, v) in f}
                    if verts & S:
                        p_hit += 1
                pent_dist = pentagons_hit_by_size.setdefault(S_size, {})
                pent_dist[p_hit] = pent_dist.get(p_hit, 0) + 1
                tot_dist = pent_total_by_size.setdefault(S_size, {})
                tot_dist[p_total] = tot_dist.get(p_total, 0) + 1
    return bad_count, by_size, is_face_boundary, pentagons_hit_by_size, pent_total_by_size


def main(max_n=20, time_budget_per_n=1800):
    print("Face structure of S-cycle in bad chord-apex+Kempe colourings.\n")
    grand_bad = 0
    grand_size = {}
    grand_face_b = {}
    grand_pent_hit = {}
    grand_pent_tot = {}
    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_bad_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}")
                break
            G.is_planar(set_embedding=True)
            D = dual_of(G)
            nb, bs, ifb, ph, pt = test_one(D)
            n_bad_n += nb
            for k, v in bs.items(): grand_size[k] = grand_size.get(k, 0) + v
            for k, v in ifb.items(): grand_face_b[k] = grand_face_b.get(k, 0) + v
            for sz, dist in ph.items():
                for k, v in dist.items():
                    grand_pent_hit.setdefault(sz, {})
                    grand_pent_hit[sz][k] = grand_pent_hit[sz].get(k, 0) + v
            for sz, dist in pt.items():
                for k, v in dist.items():
                    grand_pent_tot.setdefault(sz, {})
                    grand_pent_tot[sz][k] = grand_pent_tot[sz].get(k, 0) + v
        elapsed = time.time() - start
        print(f"n={n}: {n_bad_n} bad colourings [{elapsed:.0f}s]")
        sys.stdout.flush()
        grand_bad += n_bad_n
    print()
    print("=" * 70)
    print(f"Total bad colourings: {grand_bad}")
    print("\nIs S-cycle a face boundary of reduced dual?")
    for sz in sorted(grand_size):
        tot = grand_size[sz]
        face_count = grand_face_b.get(sz, 0)
        pct = 100 * face_count / max(tot, 1)
        print(f"  |S| = {sz}: {face_count} / {tot} ({pct:.1f}%) yes")
    print("\nDistribution of # G'-pentagons HIT by S, by |S|:")
    for sz in sorted(grand_pent_hit):
        print(f"  |S| = {sz}:")
        for h, c in sorted(grand_pent_hit[sz].items()):
            print(f"    {h} pentagons hit: {c}")
    print("\nDistribution of # G'-pentagons TOTAL, by |S|:")
    for sz in sorted(grand_pent_tot):
        print(f"  |S| = {sz}:")
        for t, c in sorted(grand_pent_tot[sz].items()):
            print(f"    {t} pentagons total: {c}")
    print("\nKey check: is # hit ≤ |S| always?")
    for sz in sorted(grand_pent_hit):
        max_hit = max(grand_pent_hit[sz].keys())
        leq = max_hit <= sz
        print(f"  |S| = {sz}: max # hit = {max_hit}, "
              f"{'≤' if leq else '>'} |S| ({'✓' if leq else '✗ violated'})")


if __name__ == '__main__':
    main()
