"""Larger empirical test of the cut-tire tree structure claim.

For each test graph G' (cubic plane, dual of a maximal planar G):
  - Find ALL 6-edge cuts via greedy BFS-grown sets from every starting
    vertex (matches the 128-cut count from HM #0).
  - For each cut, both sides:
    - Build cut tires.
    - Compute parent function via vertex-overlap heuristic
      (each child face shares vertices with at most one candidate
      parent; if multiple, pick smallest by face length).
    - Verify tree property: every cut tire has ≤ 1 parent.
    - Check for cycles in the parent relation.

Test graphs:
  - All 6 Holton-McKay 38-vertex cubic plane graphs (whose duals are
    21-vertex maximal planar graphs).
  - Dodecahedron (20 vertices, dual of icosahedron, smaller test
    case).
"""
import os
import sys
from collections import defaultdict

from sage.all import Graph, graphs

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from cut_depth_label import (
    parse_planar_code, HM_FILE,
    apply_procedure, compute_nice_layout,
)
from cut_tire import cut_tire_at
from cut_tire_tree import compute_H_d_faces, find_parent_face, build_tree


def all_six_edge_cuts(G, max_cuts=None):
    """Find all distinct 6-edge cuts via greedy BFS-grown sets."""
    n = G.order()
    seen = set()
    cuts = []
    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 seen:
                    cut_edges = [(u, v) for u in S
                                 for v in G.neighbors(u) if v not in S]
                    if len(cut_edges) == 6:
                        seen.add(S_fro)
                        cuts.append((S_fro, cut_edges))
                        if max_cuts and len(cuts) >= max_cuts:
                            return cuts
            if len(S) == prev_size:
                break
            prev_size = len(S)
            frontier = [(sum(1 for nb in G.neighbors(w) if nb in S), w)
                        for w in G.vertices()
                        if w not in S and any(nb in S
                                              for nb in G.neighbors(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
    return cuts


def is_tree(parent_of):
    """Check whether the parent relation defines a tree (no cycles).
    Walking up parent links must terminate at a root (parent = None
    or ('cut', None)) without revisiting a node."""
    nodes = list(parent_of.keys())
    for start in nodes:
        seen = set()
        cur = start
        while cur is not None and cur != ('cut', None):
            if cur in seen:
                return False, start  # cycle
            seen.add(cur)
            cur = parent_of.get(cur)
        # Terminated normally (no cycle from this start)
    return True, None


def verify_tree_on_cut(G, cut, side_label, S_side, base_pos,
                       pendant_start_id):
    """Build cut tires on this side and verify tree structure."""
    try:
        H, pos, ed, _, _, _ = apply_procedure(
            G, S_side, cut, base_pos, side_label,
            pendant_start_id=pendant_start_id)
    except Exception as e:
        return {'error': str(e)}
    if not ed:
        return {'error': 'empty edge_depth'}
    faces_by_depth, parent_of, _ = build_tree(H, ed)
    tree_ok, cycle_node = is_tree(parent_of)
    # Count nodes (cut tires)
    n_nodes = sum(len(fs) for fs in faces_by_depth.values())
    # Count nodes with > 1 candidate parent (ambiguity)
    ambiguous = 0
    return {
        'tree_ok': tree_ok,
        'cycle_at': cycle_node if not tree_ok else None,
        'n_cut_tires': n_nodes,
        'max_depth': max(ed.values()),
    }


def build_dodecahedron():
    """Build the dodecahedron graph (3-regular, 20 vertices)."""
    G = graphs.DodecahedralGraph()
    G.is_planar(set_embedding=True)
    return G


def main():
    test_graphs = []
    gs = parse_planar_code(HM_FILE)
    for i, hm in enumerate(gs):
        test_graphs.append((f'HM_{i}', hm))
    test_graphs.append(('Dodecahedron', build_dodecahedron()))

    print(f'Testing tree structure on {len(test_graphs)} graphs')

    overall_stats = {
        'total_cuts': 0,
        'total_tires': 0,
        'tree_failures': 0,
        'failures': [],
    }

    for name, G in test_graphs:
        print(f'\n=== {name}: V={G.order()}, E={G.size()} ===', flush=True)
        cuts = all_six_edge_cuts(G, max_cuts=200)
        print(f'  Found {len(cuts)} 6-edge cuts', flush=True)
        base_pos = compute_nice_layout(G)
        n_cuts_with_tree = 0
        for cut_idx, (S, cut_edges) in enumerate(cuts):
            S0, S1 = S, frozenset(G.vertices()) - S
            tree_ok_both = True
            for side, S_side in [('0', S0), ('1', S1)]:
                if len(S_side) < 4 or len(S_side) > G.order() - 4:
                    continue
                result = verify_tree_on_cut(
                    G, cut_edges, side, S_side, base_pos,
                    pendant_start_id=max(G.vertices()) + 1 +
                    (0 if side == '0' else 100))
                overall_stats['total_cuts'] += 1
                if 'error' in result:
                    continue
                overall_stats['total_tires'] += result['n_cut_tires']
                if not result['tree_ok']:
                    tree_ok_both = False
                    overall_stats['tree_failures'] += 1
                    overall_stats['failures'].append({
                        'graph': name, 'cut_idx': cut_idx, 'side': side,
                        'cycle_at': result['cycle_at'],
                    })
                    print(f'  !!! CYCLE: cut #{cut_idx}, side {side}: '
                          f'{result["cycle_at"]}', flush=True)
            if tree_ok_both:
                n_cuts_with_tree += 1
        print(f'  Cuts producing trees on both sides: '
              f'{n_cuts_with_tree}/{len(cuts)}', flush=True)

    print(f'\n=== Final summary ===', flush=True)
    print(f'Total (graph, cut, side) triples tested: {overall_stats["total_cuts"]}')
    print(f'Total cut tires examined: {overall_stats["total_tires"]}')
    print(f'Tree-structure failures (cycles): {overall_stats["tree_failures"]}')
    if overall_stats['failures']:
        print('Failures:')
        for f in overall_stats['failures'][:10]:
            print(f'  {f}')


if __name__ == '__main__':
    main()
