"""Boundary cut tire T_∂: identification + chain DP integration.

T_∂^(i) on side i:
  - Cycle = boundary walk of the unique low-side face of H_1
    (the face containing pendants).
  - OUT pendants = depth-0 edges (pendants) inside f_∂.
  - IN pendants = depth-2 edges in adjacent high-side H_1 faces
    (= the "third edge" at boundary vertices when it's depth 2).

Empirical test: extended chain DP including T_∂ should yield
non-empty R_i matching the ground-truth projection of G'_i's
3-edge colorings.
"""
import os
import sys
import itertools
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_tree import build_tree, compute_H_d_faces
from tree_structure_sweep import all_six_edge_cuts


def classify_h1_faces(H, edge_depth):
    """Compute the faces of H_1, classify each as low-side or
    high-side based on its interior content (depth-0 vs depth->=2).
    Returns (low_face, high_faces) where low_face is the (unique)
    low-side face's boundary walk."""
    faces, H1 = compute_H_d_faces(H, edge_depth, 1)
    if not faces:
        return None, []
    # For each face, examine the interior: pick an interior point and
    # ask which depth edges of G'_i lie inside.  Topologically: take
    # face's interior as a connected region, find which non-H_1 edges
    # lie strictly inside.
    # Simpler combinatorial approach: a face f of H_1 is high-side iff
    # every non-H_1 edge incident to a boundary vertex of f, on the
    # f-side, has depth >= 2.  Low-side iff at least one such has
    # depth 0.
    # Since we can't easily check "f-side", use the simpler test:
    # face is low-side iff ANY pendant (depth-0 edge) is incident to
    # a vertex of f.
    pendant_verts = set()
    for e, d in edge_depth.items():
        if d == 0:
            for v in e:
                pendant_verts.add(v)
    classified = []
    for face in faces:
        face_verts = set()
        for u, v in face:
            face_verts.add(u); face_verts.add(v)
        is_low = bool(face_verts & pendant_verts)
        classified.append((face, is_low))
    low_faces = [f for f, low in classified if low]
    high_faces = [f for f, low in classified if not low]
    if len(low_faces) > 1:
        # Choose the one with the most pendant incidences
        low_faces.sort(key=lambda f: -sum(
            1 for u, v in f
            if u in pendant_verts or v in pendant_verts))
    low_face = low_faces[0] if low_faces else None
    return low_face, high_faces


def boundary_tire_structure(H, edge_depth, low_face):
    """Construct T_∂'s structural data:
       - cycle_edges: depth-1 edges in low_face's boundary walk
                      (with multiplicity = how many times traversed
                      in the walk, but we use each edge once for
                      cycle_eids; the walk structure is implicit).
       - out_pendants: pendant edges (depth 0) at boundary vertices.
       - in_pendants: depth-2 edges incident to boundary vertices,
                      on the deeper side.
       - edges_at_v: per-vertex incident edge IDs (for proper-coloring
                      constraint).
       - all_edges: list of full edge tuples in T_∂'s order.
    """
    if low_face is None:
        return None
    # Cycle edges: unique edges in low_face's boundary walk
    cycle_e_set = set()
    cycle_edges = []
    for u, v in low_face:
        e = tuple(sorted((u, v)))
        if e not in cycle_e_set:
            cycle_e_set.add(e)
            cycle_edges.append(e)
    boundary_verts = set()
    for a, b in cycle_edges:
        boundary_verts.add(a); boundary_verts.add(b)
    # Pendants at boundary vertices
    out_pendants = []  # depth-0 edges incident
    in_pendants = []   # depth-2 edges incident
    for v in boundary_verts:
        for nb in H.neighbors(v):
            e = tuple(sorted((v, nb)))
            if e in cycle_e_set:
                continue
            de = edge_depth.get(e)
            if de == 0:
                out_pendants.append(e)
            elif de == 2:
                in_pendants.append(e)
    # Dedup
    out_pendants = list(set(out_pendants))
    in_pendants = list(set(in_pendants))
    # Construct edge ID list
    all_edges = cycle_edges + in_pendants + out_pendants
    edge_id = {e: i for i, e in enumerate(all_edges)}
    edges_at_v = defaultdict(list)
    for a, b in cycle_edges:
        edges_at_v[a].append(edge_id[(a, b)])
        edges_at_v[b].append(edge_id[(a, b)])
    for e in in_pendants + out_pendants:
        for v in e:
            if v in boundary_verts:
                edges_at_v[v].append(edge_id[e])
    return {
        'all_edges': all_edges,
        'edge_id': edge_id,
        'edges_at_v': dict(edges_at_v),
        'cycle_eids': [edge_id[e] for e in cycle_edges],
        'in_eids': [edge_id[e] for e in in_pendants],
        'out_eids': [edge_id[e] for e in out_pendants],
        'low_face': low_face,
    }


def enumerate_proper(struct, max_edges=18):
    """Enumerate proper 3-edge-colorings of struct's edges,
    respecting per-vertex distinct-color constraint."""
    n_e = len(struct['all_edges'])
    if n_e > max_edges:
        return None
    out = []
    for assign in itertools.product([0, 1, 2], repeat=n_e):
        ok = True
        for v, eids in struct['edges_at_v'].items():
            cs = [assign[i] for i in eids]
            if len(set(cs)) != len(cs):
                ok = False; break
        if ok:
            out.append(assign)
    return out


def proper_3_edge_colorings_full(G, max_n=60):
    """Brute-force backtracking enum of all proper 3-edge-colorings."""
    edges = [tuple(sorted(e[:2])) for e in G.edges()]
    n = len(edges)
    if n > max_n:
        return None
    eav = defaultdict(list)
    for i, (u, v) in enumerate(edges):
        eav[u].append(i)
        eav[v].append(i)
    colors = [-1] * n
    results = []
    def bt(i):
        if i == n:
            results.append(tuple(colors))
            return
        u, v = edges[i]
        used = set()
        for j in eav[u]:
            if colors[j] != -1: used.add(colors[j])
        for j in eav[v]:
            if colors[j] != -1: used.add(colors[j])
        for c in range(3):
            if c in used: continue
            colors[i] = c
            bt(i + 1)
            colors[i] = -1
    bt(0)
    return [{edges[i]: c for i, c in enumerate(r)} for r in results]


def project_to_pendants(colorings, pendant_edges):
    """Project G'_i colorings to a fixed list of pendant edges.
    Returns a set of color tuples (in the order of pendant_edges)."""
    out = set()
    for c in colorings:
        try:
            out.add(tuple(c[tuple(sorted(e[:2]))] for e in pendant_edges))
        except KeyError:
            continue
    return out


def s3_orbit(t):
    return {tuple(p[c] for c in t)
            for p in itertools.permutations([0, 1, 2])}


def has_full_s3(s):
    if not s: return False
    for t in s:
        orb = s3_orbit(t)
        if len(orb) == 6 and orb <= s:
            return True
    return False


def chain_dp_with_boundary(H, edge_depth):
    """Compute the chain DP including the boundary cut tire T_∂.

    Algorithm:
      1. Build high-side forest as before; compute per-tire A(T).
      2. Build T_∂ structure from low-side H_1 face.
      3. Compute A(T_∂) = proper 3-edge-colorings of T_∂.
      4. Restrict A(T_∂) by edge-sharing with adjacent high-side
         tires (depth-1 boundary edges, depth-2 in-pendants).
      5. Project to OUT pendants = R_i^DP.
    """
    if not edge_depth:
        return set(), {}
    faces_by_depth, parent_of, _ = build_tree(H, edge_depth)
    # Build per-tire structures (joint coloring DP)
    from chain_dp_joint import tire_edges, enumerate_proper as ep, chain_dp_joint
    A_high, tire_struct, _ = chain_dp_joint(
        faces_by_depth, parent_of, H, edge_depth)

    # Boundary tire
    low_face, high_faces = classify_h1_faces(H, edge_depth)
    bdy_struct = boundary_tire_structure(H, edge_depth, low_face)
    if bdy_struct is None:
        return set(), {'reason': 'no low_face'}

    bdy_cs = enumerate_proper(bdy_struct, max_edges=18)
    if bdy_cs is None:
        return set(), {'reason': 'bdy too big',
                       'n_edges': len(bdy_struct['all_edges'])}

    # Restrict A(T_∂) by sharing with high-side tires.
    # For each adjacent high-side tire that shares any T_∂ edges,
    # require T_∂ coloring to be compatible with some A(tire) coloring
    # on shared edges.
    bdy_eid = bdy_struct['edge_id']
    bdy_edges_set = set(bdy_struct['all_edges'])
    kid_shared = []
    for node, struct in tire_struct.items():
        if A_high.get(node) is None or not A_high.get(node):
            continue
        node_edges = set(struct['all_edges'])
        shared = bdy_edges_set & node_edges
        if not shared:
            continue
        shared_pairs = []  # (bdy_eid, node_eid)
        for e in shared:
            shared_pairs.append((bdy_eid[e], struct['edge_id'][e]))
        # Compute set of shared-coloring keys this node accepts
        accepted = set()
        for assign in A_high[node]:
            key = tuple(assign[neid] for (_, neid) in shared_pairs)
            accepted.add(key)
        kid_shared.append((shared_pairs, accepted, node))
    # Filter bdy_cs
    survivors = []
    for assign in bdy_cs:
        ok = True
        for shared_pairs, accepted, _ in kid_shared:
            key = tuple(assign[beid] for (beid, _) in shared_pairs)
            if key not in accepted:
                ok = False; break
        if ok:
            survivors.append(assign)
    # Project to OUT pendants
    out_eids = bdy_struct['out_eids']
    R = set()
    for assign in survivors:
        R.add(tuple(assign[i] for i in out_eids))
    return R, {
        'bdy_n_edges': len(bdy_struct['all_edges']),
        'bdy_cycle_eids': len(bdy_struct['cycle_eids']),
        'bdy_out': len(bdy_struct['out_eids']),
        'bdy_in': len(bdy_struct['in_eids']),
        'bdy_valid_pre': len(bdy_cs),
        'bdy_valid_post': len(survivors),
        'high_neighbors': len(kid_shared),
    }


def test_one_cut(G, cut_idx, cuts, base_pos, verbose=False):
    S, cut_edges = cuts[cut_idx]
    S0, S1 = S, frozenset(G.vertices()) - S
    if len(S0) < 3 or len(S1) < 3:
        return None
    if verbose:
        print(f'  cut #{cut_idx}: |S0|={len(S0)}, |S1|={len(S1)}',
              flush=True)
    results = {'cut_idx': cut_idx}
    for side, S_side in [('0', S0), ('1', S1)]:
        try:
            H, _, ed, _, _, _ = apply_procedure(
                G, S_side, cut_edges, base_pos, side,
                pendant_start_id=max(G.vertices()) + 1 +
                (0 if side == '0' else 500))
        except Exception as e:
            results[f'side_{side}'] = {'error': str(e)}
            continue
        if not ed:
            continue
        # Ground truth on G'_i
        gi_cols = proper_3_edge_colorings_full(H, max_n=50)
        if gi_cols is None:
            R_ground = None
        else:
            # Project to pendants (depth-0)
            pendant_edges = [e for e, d in ed.items() if d == 0]
            R_ground = project_to_pendants(gi_cols, pendant_edges)

        R_dp, meta = chain_dp_with_boundary(H, ed)
        results[f'side_{side}'] = {
            'R_ground_size': len(R_ground) if R_ground is not None else None,
            'R_ground_full_s3': has_full_s3(R_ground)
                if R_ground is not None else None,
            'R_dp_size': len(R_dp),
            'R_dp_full_s3': has_full_s3(R_dp),
            'match': R_ground == R_dp if R_ground is not None else None,
            'meta': meta,
        }
        if verbose:
            r = results[f'side_{side}']
            print(f'    side {side}: |R_ground|={r["R_ground_size"]}, '
                  f'|R_dp|={r["R_dp_size"]}, match={r["match"]}, '
                  f'meta={r["meta"]}',
                  flush=True)
    return results


def main():
    print('=== Dodecahedron ===', flush=True)
    G = graphs.DodecahedralGraph()
    G.is_planar(set_embedding=True)
    base_pos = compute_nice_layout(G)
    cuts = all_six_edge_cuts(G, max_cuts=5)
    for i in range(min(5, len(cuts))):
        test_one_cut(G, i, cuts, base_pos, verbose=True)

    print('\n=== HM_0 ===', flush=True)
    gs = parse_planar_code(HM_FILE)
    cuts = all_six_edge_cuts(gs[0], max_cuts=3)
    base_pos = compute_nice_layout(gs[0])
    for i in range(min(3, len(cuts))):
        test_one_cut(gs[0], i, cuts, base_pos, verbose=True)


if __name__ == '__main__':
    main()
