"""Broader empirical sweep of the (k >= 2) loose chain pigeonhole
conjecture on cut tires.

For each of the 6 Holton-McKay graphs:
  - Find multiple 6-edge cuts (preferring matching cuts).
  - For each cut, build cut tires on both halves at every depth.
  - For each cut tire with total spoke count k >= 2, check whether
    |π(T)| >= 6.

Report any cut tire with k >= 2 and |π(T)| < 6 (= counterexample).
"""
import os
import sys
from itertools import permutations, product

from sage.all import Graph

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from cut_depth_label import (
    parse_planar_code, HM_FILE, find_six_edge_cut,
    apply_procedure, compute_nice_layout,
)
from cut_tire import cut_tire_at
from cut_tire_test import build_cut_tire_graph, enumerate_proper_edge_3colorings


def find_multiple_six_cuts(G, max_cuts=5):
    """Find multiple distinct 6-edge cuts via greedy search from
    different starts."""
    n = G.order()
    candidates_seen = set()
    candidates_list = []
    for start in G.vertices():
        S = {start}
        boundary = 3
        prev_size = -1
        while True:
            if boundary == 6 and 2 <= len(S) <= n - 2:
                S_fro = frozenset(S)
                if S_fro not in candidates_seen:
                    cut = [(u, v) for u in S for v in G.neighbors(u)
                           if v not in S]
                    if len(cut) == 6:
                        candidates_seen.add(S_fro)
                        endpoints_in_S = [u for (u, v) in cut if u in S] + \
                                         [v for (u, v) in cut if v in S]
                        endpoints_in_Sc = [u for (u, v) in cut if u not in S] + \
                                          [v for (u, v) in cut if v not in S]
                        is_matching = (len(set(endpoints_in_S)) == 6 and
                                       len(set(endpoints_in_Sc)) == 6)
                        balance = abs(len(S) - n / 2)
                        candidates_list.append((S_fro, cut, is_matching,
                                                balance))
            if len(S) == prev_size:
                break
            prev_size = len(S)
            frontier = []
            for w in G.vertices():
                if w in S: continue
                if any(nb in S for nb in G.neighbors(w)):
                    edges_into_S = sum(1 for nb in G.neighbors(w) if nb in S)
                    frontier.append((edges_into_S, w))
            if not frontier:
                break
            frontier.sort(reverse=True)
            _, w = frontier[0]
            edges_into_S = sum(1 for nb in G.neighbors(w) if nb in S)
            S.add(w)
            boundary = boundary + 3 - 2 * edges_into_S
            if len(S) >= n - 1:
                break
    # Sort: matching first, then balanced
    candidates_list.sort(key=lambda x: (-int(x[2]), x[3]))
    return [(S, cut) for (S, cut, _, _) in candidates_list[:max_cuts]]


def quick_projection(tire):
    """Build the cut tire graph and brute-force enumerate proper
    edge 3-colorings to compute |π(T)| and orbit decomposition.
    Skip if too many edges."""
    G_tire, edge_labels, _ = build_cut_tire_graph(tire, 0)
    n_edges = G_tire.size()
    if n_edges > 14:
        return None
    edges_norm, colorings = enumerate_proper_edge_3colorings(G_tire)
    out_idx = [i for i, e in enumerate(edges_norm) if edge_labels[e] == 'out']
    in_idx = [i for i, e in enumerate(edges_norm) if edge_labels[e] == 'in']
    proj = set()
    for c in colorings:
        sigma = (tuple(c[i] for i in out_idx), tuple(c[i] for i in in_idx))
        proj.add(sigma)
    # Orbit decomposition
    seen = set()
    orbit_sizes = []
    has_full_orbit = False
    for sigma in sorted(proj):
        if sigma in seen: continue
        orbit = set()
        for pi in permutations((1, 2, 3)):
            mapped = (tuple(pi[c - 1] for c in sigma[0]),
                      tuple(pi[c - 1] for c in sigma[1]))
            orbit.add(mapped)
        orbit_in_proj = orbit & proj
        orbit_sizes.append(len(orbit_in_proj))
        if len(orbit_in_proj) == 6:
            has_full_orbit = True
        seen |= orbit_in_proj
    return {
        'n_edges': n_edges,
        'n_colorings': len(colorings),
        'proj_size': len(proj),
        'orbit_sizes': sorted(orbit_sizes),
        'has_full_orbit': has_full_orbit,
        'k_total': len(out_idx) + len(in_idx),
    }


def sweep():
    gs = parse_planar_code(HM_FILE)
    print(f'Loaded {len(gs)} Holton-McKay graphs\n')

    counterexamples = []
    n_tires_tested = 0
    n_k_ge_2 = 0
    n_passed = 0

    for hm_idx, G in enumerate(gs):
        print(f'=== HM #{hm_idx} ===', flush=True)
        try:
            cuts = find_multiple_six_cuts(G, max_cuts=3)
        except Exception as e:
            print(f'  cut search failed: {e}')
            continue
        print(f'  Found {len(cuts)} candidate cuts to test', flush=True)
        base_pos = compute_nice_layout(G)
        for cut_idx, (S, cut) in enumerate(cuts):
            S0 = frozenset(S)
            S1 = frozenset(G.vertices()) - S0
            print(f'  Cut #{cut_idx}: |S|={len(S0)}, '
                  f'matching={len(set(u for (u,v) in cut if u in S0) | set(v for (u,v) in cut if v in S0)) == 6 and len(set(u for (u,v) in cut if u not in S0) | set(v for (u,v) in cut if v not in S0)) == 6}',
                  flush=True)
            for side, S_side in [('0', S0), ('1', S1)]:
                if len(S_side) < 4 or len(S_side) > G.order() - 4:
                    continue
                try:
                    H, pos, ed, _, _, _ = apply_procedure(
                        G, S_side, cut, base_pos, side,
                        pendant_start_id=max(G.vertices()) + 1 + (
                            0 if side == '0' else 50))
                except Exception as e:
                    print(f'    side {side}: apply_procedure failed: {e}',
                          flush=True)
                    continue
                if not ed:
                    continue
                d_max = max(ed.values())
                tires_examined_side = 0
                tires_passed_side = 0
                for d in range(1, d_max + 1):
                    tires = cut_tire_at(H, ed, d)
                    for f_idx, tire in enumerate(tires):
                        k = len(tire['out_spokes']) + len(tire['in_spokes'])
                        n_tires_tested += 1
                        tires_examined_side += 1
                        if k < 2:
                            continue
                        n_k_ge_2 += 1
                        result = quick_projection(tire)
                        if result is None:
                            continue
                        if not result['has_full_orbit']:
                            counterexamples.append({
                                'hm': hm_idx, 'cut_idx': cut_idx,
                                'side': side, 'd': d, 'face': f_idx,
                                'k': k, 'result': result,
                            })
                            print(f'    !!! COUNTEREXAMPLE: side {side}, '
                                  f'depth {d}, face {f_idx}: '
                                  f'k={k}, |π|={result["proj_size"]}, '
                                  f'orbits={result["orbit_sizes"]}',
                                  flush=True)
                        else:
                            n_passed += 1
                            tires_passed_side += 1
                print(f'    side {side}: max_depth={d_max}, '
                      f'tires={tires_examined_side}, '
                      f'k>=2 passed={tires_passed_side}',
                      flush=True)

    print(f'\n=== Summary ===')
    print(f'Total cut tires examined: {n_tires_tested}')
    print(f'Cut tires with k >= 2: {n_k_ge_2}')
    print(f'k>=2 tires passing (|π| >= 6 with full S_3 orbit): {n_passed}')
    print(f'Counterexamples (k>=2 without full orbit): {len(counterexamples)}')

    if counterexamples:
        print('\nCounterexample details:')
        for ce in counterexamples:
            print(f'  HM#{ce["hm"]}, cut {ce["cut_idx"]}, side {ce["side"]}, '
                  f'd={ce["d"]}, face={ce["face"]}, k={ce["k"]}, '
                  f'|π|={ce["result"]["proj_size"]}, '
                  f'orbits={ce["result"]["orbit_sizes"]}')


if __name__ == '__main__':
    sweep()
