"""
Two-phase resolution algorithm test:
  Phase 1: depth-guided flip loop to resolve tricky-everywhere odd cycles.
  Phase 2: for each odd simple cycle of L_k incident to the outer face,
           flip one (outer-incident) edge. Multi-edges allowed; we work in
           the simple-graph collapse where add_edge on an existing edge is
           a no-op (which is equivalent to multigraph bipartiteness).

Success criterion: at the end, the induced subgraph on each level k is
bipartite (no odd cycle).
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import networkx as nx
from collections import defaultdict
from triangulation_gen import enumerate_all_triangulations
from level_cycles import compute_levels, level_sources
from depth_monovariant_check import (
    faces_of_subgraph, canonical_face, edges_of_face,
    apex_levels_for_edge, identify_outer_face, face_depths,
)


def outer_edge_count(face, outer_face_edges):
    return sum(1 for (a, b) in edges_of_face(face)
                if frozenset([a, b]) in outer_face_edges)


def interior_odd_faces(faces, depths, outer_canon, outer_face_edges):
    """Odd inner faces with depth > 0 AND no edges incident to the outer face."""
    out = []
    for face in faces:
        cf = canonical_face(face)
        if cf == outer_canon:
            continue
        if len(face) % 2 != 1:
            continue
        d = depths.get(cf)
        if d is None or d <= 0:
            continue
        if outer_edge_count(face, outer_face_edges) > 0:
            continue
        out.append((cf, face, d))
    return out


def shared_edge_to_lowest_neighbor(face, G, emb_G, levels, k, depths, faces,
                                    outer_canon):
    """Find the inner face F' adjacent to `face` with minimum facial depth,
    and return the flip on the edge shared between face and F'."""
    face_canon = canonical_face(face)
    face_edges = set(frozenset([a, b]) for (a, b) in edges_of_face(face))
    best = None  # (other_depth, u, v, w, x)
    for f in faces:
        cf = canonical_face(f)
        if cf == face_canon or cf == outer_canon:
            continue
        # Find shared edge between face and f.
        shared = None
        for (a, b) in edges_of_face(f):
            if frozenset([a, b]) in face_edges:
                shared = (a, b)
                break
        if shared is None:
            continue
        other_depth = depths.get(cf)
        if other_depth is None:
            continue
        u, v = shared
        if not G.has_edge(u, v):
            continue
        result = apex_levels_for_edge(G, emb_G, u, v, levels)
        if result is None:
            continue
        _, (w, x) = result
        if G.has_edge(w, x):
            continue
        if best is None or other_depth < best[0]:
            best = (other_depth, u, v, w, x)
    if best is None:
        return None
    _, u, v, w, x = best
    return (u, v, w, x)


def phase1_resolve_isolated(G, levels, nodes_k, k, max_steps=400):
    """Per spec:
      (1) take the dual of L_k;
      (2) depth 0 = inner face with >=2 outer-face edges;
      (3) other faces' depth = BFS distance to a depth-0 face;
      (4) find odd C of greatest depth with 0 outer-face edges;
      (5) flip the edge shared with the lowest-depth incident neighbor.
    Repeat until no such C exists."""
    Gc = G.copy()
    ip, emb_c = nx.check_planarity(Gc)
    for step in range(max_steps):
        faces = faces_of_subgraph(Gc, emb_c, nodes_k)
        if len(faces) < 2:
            return Gc, emb_c, step, 'no_faces'
        outer = identify_outer_face(faces, Gc, emb_c, levels, k)
        if outer is None:
            return Gc, emb_c, step, 'no_outer'
        outer_face = next((f for f in faces if canonical_face(f) == outer),
                           None)
        outer_face_edges = set()
        if outer_face is not None:
            for (a, b) in edges_of_face(outer_face):
                outer_face_edges.add(frozenset([a, b]))
        depths = face_depths(faces, outer)
        if depths is None:
            depths = {}
        interior = interior_odd_faces(faces, depths, outer, outer_face_edges)
        if not interior:
            return Gc, emb_c, step, 'done'
        cf_t, face_t, d_t = max(interior, key=lambda x: x[2])
        flip = shared_edge_to_lowest_neighbor(
            face_t, Gc, emb_c, levels, k, depths, faces, outer)
        if flip is None:
            return Gc, emb_c, step, 'no_flip'
        u, v, w, x = flip
        Gc.remove_edge(u, v)
        Gc.add_edge(w, x)
        ip, emb_c = nx.check_planarity(Gc)
        if not ip:
            return Gc, emb_c, step, 'nonplanar'
    return Gc, emb_c, max_steps, 'budget'


def phase2_flip_outer_incident(G, emb, levels, nodes_k, k):
    """Flip one edge per odd simple cycle of L_k incident to outer face.
    Multi-edges allowed (simple-graph add is a no-op, equivalent to multigraph
    bipartiteness)."""
    faces = faces_of_subgraph(G, emb, nodes_k)
    if not faces:
        return G.copy(), []
    outer = identify_outer_face(faces, G, emb, levels, k)
    if outer is None:
        return G.copy(), []
    outer_face = next((f for f in faces if canonical_face(f) == outer), None)
    if outer_face is None:
        return G.copy(), []
    outer_edges = set(frozenset(list(e)) for e in edges_of_face(outer_face))
    Gn = G.copy()
    flips = []
    for face in faces:
        cf = canonical_face(face)
        if cf == outer:
            continue
        if len(face) % 2 != 1:
            continue
        # Incident to outer face?
        face_edges = [frozenset([a, b]) for (a, b) in edges_of_face(face)]
        if not any(e in outer_edges for e in face_edges):
            continue
        # Prefer the outer-incident edge for the flip.
        chosen = None
        for (u, v) in edges_of_face(face):
            if frozenset([u, v]) not in outer_edges:
                continue
            if not Gn.has_edge(u, v):
                continue
            result = apex_levels_for_edge(Gn, emb, u, v, levels)
            if result is None:
                continue
            _, (w, x) = result
            chosen = (u, v, w, x)
            break
        if chosen is None:
            continue
        u, v, w, x = chosen
        Gn.remove_edge(u, v)
        # add_edge on existing edge in nx.Graph is a no-op (multigraph collapse)
        Gn.add_edge(w, x)
        flips.append((u, v, w, x))
    return Gn, flips


def is_Lk_bipartite(G, nodes_k):
    Lk = G.subgraph(nodes_k)
    return nx.is_bipartite(Lk)


def run_check(n_values, max_steps=200):
    total = 0
    p1_failed = 0
    p2_success = 0
    p2_failure = 0
    parity_bipartite = 0
    failures = []

    for n in n_values:
        print(f"\n=== n = {n} ===")
        tris = enumerate_all_triangulations(n)
        print(f"  {len(tris)} iso-classes")
        for tri_idx, G in enumerate(tris):
            ip, emb = nx.check_planarity(G)
            if not ip:
                continue
            for kind, label, source_set in level_sources(G, emb):
                levels = compute_levels(G, source_set)
                by_level = defaultdict(list)
                for v, lv in levels.items():
                    by_level[lv].append(v)
                # Run on every level k>=1 with at least one odd cycle.
                for k, nodes_k in by_level.items():
                    if k == 0 or len(nodes_k) < 3:
                        continue
                    faces = faces_of_subgraph(G, emb, nodes_k)
                    if len(faces) < 2:
                        continue
                    has_odd = any(
                        len(f) % 2 == 1
                        and canonical_face(f) != canonical_face(
                            max(faces, key=len))
                        for f in faces
                    )
                    if not has_odd:
                        continue
                    total += 1
                    G1, emb1, p1_steps, p1_reason = phase1_resolve_isolated(
                        G, levels, nodes_k, k, max_steps)
                    if p1_reason not in ('done', 'no_faces'):
                        p1_failed += 1
                        if len(failures) < 5:
                            failures.append({
                                'n': n, 'tri_idx': tri_idx,
                                'source': (kind, label),
                                'k': k,
                                'phase': 1, 'reason': p1_reason
                            })
                        continue
                    G2, flips = phase2_flip_outer_incident(
                        G1, emb1, levels, nodes_k, k)
                    if is_Lk_bipartite(G2, nodes_k):
                        p2_success += 1
                        # Also check the parity subgraph bipartiteness.
                        # Parity = even / odd in BFS levels.
                        even_nodes = [v for v in G2.nodes()
                                       if levels.get(v, 0) % 2 == 0]
                        odd_nodes = [v for v in G2.nodes()
                                      if levels.get(v, 0) % 2 == 1]
                        even_sub = G2.subgraph(even_nodes)
                        odd_sub = G2.subgraph(odd_nodes)
                        if nx.is_bipartite(even_sub) and \
                           nx.is_bipartite(odd_sub):
                            parity_bipartite += 1
                    else:
                        p2_failure += 1
                        if len(failures) < 5:
                            Lk_after = G2.subgraph(nodes_k)
                            failures.append({
                                'n': n, 'tri_idx': tri_idx,
                                'source': (kind, label),
                                'k': k, 'phase': 2,
                                'reason': 'Lk_not_bipartite',
                                'flips': flips,
                                'Lk_edges': sorted(Lk_after.edges()),
                            })
    print(f"\n=== summary ===")
    print(f"configs with odd cycles in some L_k: {total}")
    print(f"  Phase 1 succeeded: {total - p1_failed}")
    print(f"  Phase 1 failed:    {p1_failed}")
    print(f"  Phase 2 -> L_k bipartite:   {p2_success}")
    print(f"  Phase 2 -> L_k still has odd cycle: {p2_failure}")
    print(f"  Of bipartite L_k: full parity-subgraph bipartite: "
          f"{parity_bipartite}")
    if failures:
        print(f"\nfailures (first {len(failures)}):")
        for f in failures:
            print(f"  {f}")


if __name__ == "__main__":
    if len(sys.argv) > 1:
        ns = [int(x) for x in sys.argv[1:]]
    else:
        ns = [9, 10]
    run_check(ns)
