"""For the 1,314 bad chord-apex+Kempe colourings (where
Lemma flank-covering-hex empirically fails), characterize the set
S = V \\ (V(K_b) ∪ V(K_c)) -- the "uncovered" vertices.

For each bad colouring, report:
  - |S| (size of S);
  - distribution of S-vertices' graph-distance from v_n (the spike
    vertex), and from the merged edge;
  - on which faces of the reduced dual the S-vertices sit (G'-pentagon,
    G'-hexagon, F^♭_flank, F^♭_outer, F^♭_merged, etc.);
  - per-colouring: how MANY G'-pentagons are hit by S (= how close to
    "covering all G'-pentagons" we are; the pigeonhole would need
    |S|*3 ≥ #pentagons).

The goal is to find structural patterns we could exploit in a
discharging argument.

Run with: sage experiments/characterize_S_vertices.py
"""
import os
import sys
import time
from collections import defaultdict

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 classify_face(f, named):
    """Classify a face of the reduced dual."""
    edges_set = {frozenset(e) for e in f}
    has_s0 = named['side_0'] in edges_set
    has_s1 = named['side_1'] in edges_set
    has_sp = named['spike'] in edges_set
    has_m = named['merged'] in edges_set
    if has_s0 and has_sp and not has_s1 and not has_m: return 'flank-lower'
    if has_sp and has_s1 and not has_s0 and not has_m: return 'flank-upper'
    if has_s0 and has_s1 and has_m and not has_sp: return 'outer'
    if has_m and not (has_s0 or has_s1 or has_sp): return 'merged'
    if not (has_s0 or has_s1 or has_sp or has_m): return 'G-prime'
    return 'mixed'


def test_one(D):
    D.is_planar(set_embedding=True)
    bad_count = 0
    S_size_dist = {}
    S_dist_from_vn = {}    # graph dist of S-vertex from v_n
    S_face_types = {}      # face types containing S-vertices
    pentagons_hit_dist = {} # how many G'-pentagons hit by S, per colouring
    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 P_1, P_2 on lower flank, check 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
                # Compute K_b, K_c, S
                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
                # P_1 ∉ V_union check
                if P_1 in V_union: continue
                bad_count += 1
                S_size = len(S)
                S_size_dist[S_size] = S_size_dist.get(S_size, 0) + 1
                # Distances from v_n
                for s in S:
                    try:
                        d = H.distance(s, v_n)
                    except:
                        d = -1
                    S_dist_from_vn[d] = S_dist_from_vn.get(d, 0) + 1
                # Face types containing S-vertices
                for s in S:
                    for f in H.faces():
                        verts = {u for (u, v) in f} | {v for (u, v) in f}
                        if s in verts:
                            t = classify_face(f, named)
                            S_face_types[t] = S_face_types.get(t, 0) + 1
                # # G'-pentagons hit by S
                n_pent_hit = 0
                n_pent_total = 0
                for f in H.faces():
                    if classify_face(f, named) != 'G-prime': continue
                    if len(f) != 5: continue
                    n_pent_total += 1
                    verts = {u for (u, v) in f} | {v for (u, v) in f}
                    if verts & S: n_pent_hit += 1
                pent_pair = (n_pent_hit, n_pent_total)
                pentagons_hit_dist[pent_pair] = (
                    pentagons_hit_dist.get(pent_pair, 0) + 1)
    return (bad_count, S_size_dist, S_dist_from_vn, S_face_types,
            pentagons_hit_dist)


def main(max_n=20, time_budget_per_n=1800):
    print("Structural characterization of S = V \\ (V(K_b) ∪ V(K_c))\n"
          "on the 1,314 bad chord-apex+Kempe colourings.\n")
    grand_bad = 0
    grand_S_size = {}
    grand_S_dist = {}
    grand_S_face = {}
    grand_pent_hit = {}
    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}/{len(triangulations)}")
                break
            G.is_planar(set_embedding=True)
            D = dual_of(G)
            nb, ssize, sdist, sface, phit = test_one(D)
            n_bad_n += nb
            for k, v in ssize.items(): grand_S_size[k] = grand_S_size.get(k, 0) + v
            for k, v in sdist.items(): grand_S_dist[k] = grand_S_dist.get(k, 0) + v
            for k, v in sface.items(): grand_S_face[k] = grand_S_face.get(k, 0) + v
            for k, v in phit.items(): grand_pent_hit[k] = grand_pent_hit.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("\n|S| distribution:")
    for s in sorted(grand_S_size):
        c = grand_S_size[s]
        print(f"  |S| = {s}: {c} ({100*c/max(grand_bad, 1):.2f}%)")
    print("\nGraph distance of S-vertices from v_n:")
    for d in sorted(grand_S_dist):
        c = grand_S_dist[d]
        total_S_verts = sum(grand_S_dist.values())
        print(f"  dist = {d}: {c} ({100*c/max(total_S_verts, 1):.2f}%)")
    print("\nFace types containing S-vertices:")
    total = sum(grand_S_face.values())
    for t in sorted(grand_S_face, key=lambda k: -grand_S_face[k]):
        c = grand_S_face[t]
        print(f"  {t}: {c} ({100*c/max(total, 1):.2f}%)")
    print("\n(# G'-pentagons hit by S, # G'-pentagons total) per colouring:")
    for pair in sorted(grand_pent_hit, key=lambda k: -grand_pent_hit[k])[:20]:
        c = grand_pent_hit[pair]
        h, t = pair
        ratio = h / t if t > 0 else 0
        print(f"  hit/total = {h}/{t} ({100*ratio:.0f}%): {c}")


if __name__ == '__main__':
    main()
