"""
Sanity check for the facial-depth monovariant on tricky (k,k) configurations.

For each (G, S, k) where L_k has at least one "tricky-everywhere" odd face
(every edge is a (k,k) apex pair):
  - Run the proposed algorithm: pick the deepest tricky-everywhere odd face,
    flip the edge whose other-side face has min facial depth.
  - Iterate until no tricky-everywhere odd face remains, or step budget hits.
  - Track multiple candidate monovariants per flip.

Reports counts of (a) terminations within budget, (b) cycle/failure cases,
(c) which monovariant (if any) decreases on every flip.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import networkx as nx
from collections import defaultdict, deque
from triangulation_gen import enumerate_all_triangulations
from level_cycles import (
    compute_levels, get_all_faces, inherited_embedding, level_sources
)


# -- helpers ------------------------------------------------------------

def faces_of_subgraph(G, emb_G, nodes_k):
    if len(nodes_k) < 3:
        return []
    L_k = G.subgraph(nodes_k)
    if L_k.number_of_edges() < 3:
        return []
    try:
        emb_L = inherited_embedding(emb_G, nodes_k)
        return get_all_faces(emb_L)
    except Exception:
        ip, emb_L = nx.check_planarity(L_k)
        if not ip:
            return []
        return get_all_faces(emb_L)


def canonical_face(face):
    rots = [tuple(face[i:] + face[:i]) for i in range(len(face))] \
         + [tuple(face[::-1][i:] + face[::-1][:i]) for i in range(len(face))]
    return min(rots)


def edges_of_face(face):
    n = len(face)
    return [(face[i], face[(i+1) % n]) for i in range(n)]


def apex_levels_for_edge(G, emb_G, u, v, levels):
    if not G.has_edge(u, v):
        return None
    f1 = emb_G.traverse_face(u, v)
    f2 = emb_G.traverse_face(v, u)
    if len(f1) != 3 or len(f2) != 3:
        return None
    w = next(x for x in f1 if x != u and x != v)
    x = next(y for y in f2 if y != u and y != v)
    return (levels.get(w), levels.get(x)), (w, x)


def identify_outer_face(faces, G, emb_G, levels, k):
    """Outer face = the face whose 'outside' connects to lower levels.
    Score each face by how many edges have at least one apex at level < k
    on the opposite side; pick highest score, longest boundary as tiebreak."""
    best = (-1, -1, None)
    for face in faces:
        score = 0
        for (u, v) in edges_of_face(face):
            if not G.has_edge(u, v):
                continue
            f1 = emb_G.traverse_face(u, v)
            f2 = emb_G.traverse_face(v, u)
            if len(f1) != 3 or len(f2) != 3:
                continue
            w = next(x for x in f1 if x != u and x != v)
            x = next(y for y in f2 if y != u and y != v)
            if (levels.get(w, k) < k) or (levels.get(x, k) < k):
                score += 1
        key = (score, len(face))
        if key > (best[0], best[1]):
            best = (score, len(face), face)
    return canonical_face(best[2]) if best[2] is not None else None


def face_depths(faces, outer_canon):
    """BFS depth in the dual graph from the seed set:
    inner faces with >= 2 edges incident to the outer face."""
    edge_to_faces = defaultdict(list)
    canon_set = set()
    for face in faces:
        cf = canonical_face(face)
        canon_set.add(cf)
        for (a, b) in edges_of_face(face):
            edge_to_faces[frozenset([a, b])].append(cf)
    # Outer-face edges = edges of the outer face.
    outer_face_edges = set()
    for face in faces:
        if canonical_face(face) == outer_canon:
            for (a, b) in edges_of_face(face):
                outer_face_edges.add(frozenset([a, b]))
            break
    # Seed set: inner faces with >= 2 outer-face edges.
    seeds = []
    for face in faces:
        cf = canonical_face(face)
        if cf == outer_canon:
            continue
        count = 0
        for (a, b) in edges_of_face(face):
            if frozenset([a, b]) in outer_face_edges:
                count += 1
        if count >= 2:
            seeds.append(cf)
    if not seeds:
        # Fall back: no seeds means no "easy" odd face nearby. Return empty.
        return {}
    D = nx.Graph()
    for cf in canon_set:
        if cf != outer_canon:
            D.add_node(cf)
    for e, fs in edge_to_faces.items():
        if len(fs) == 2:
            f1, f2 = fs
            if f1 != outer_canon and f2 != outer_canon:
                D.add_edge(f1, f2)
    # Multi-source BFS.
    depths = {}
    queue = deque()
    for s in seeds:
        if s in D:
            depths[s] = 0
            queue.append(s)
    while queue:
        u = queue.popleft()
        for v in D.neighbors(u):
            if v not in depths:
                depths[v] = depths[u] + 1
                queue.append(v)
    return depths


def is_both_k_everywhere(face, G, emb_G, levels, k):
    for (u, v) in edges_of_face(face):
        result = apex_levels_for_edge(G, emb_G, u, v, levels)
        if result is None:
            return False
        (la, lb), _ = result
        if (la, lb) != (k, k):
            return False
    return True


def tricky_odd_faces(faces, G, emb_G, levels, k, depths, outer_canon):
    """Return list of (canon, face, depth) for odd faces that are tricky-
    everywhere AND have at least one (k,k) edge whose other-side face is
    not the outer face (i.e., we can flip toward an inner neighbor)."""
    out = []
    for face in faces:
        cf = canonical_face(face)
        if cf == outer_canon:
            continue
        if len(face) % 2 != 1:
            continue
        if not is_both_k_everywhere(face, G, emb_G, levels, k):
            continue
        d = depths.get(cf)
        if d is None:
            continue
        out.append((cf, face, d))
    return out


def odd_faces_all(faces, depths, outer_canon):
    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:
            continue
        out.append((cf, face, d))
    return out


def lowest_depth_neighbor_flip(face, G, emb_G, levels, k, depths, faces,
                                outer_canon):
    """Among (k,k) edges of `face`, return the flip toward the lowest-depth
    non-outer neighbor face. Returns (u, v, w, x, neighbor_depth) or None."""
    edge_to_other = {}
    face_canon = canonical_face(face)
    for f in faces:
        cf = canonical_face(f)
        if cf == face_canon:
            continue
        for (a, b) in edges_of_face(f):
            e = frozenset([a, b])
            edge_to_other.setdefault(e, cf)
    best = None
    for (u, v) in edges_of_face(face):
        result = apex_levels_for_edge(G, emb_G, u, v, levels)
        if result is None:
            continue
        (la, lb), (w, x) = result
        if (la, lb) != (k, k):
            continue
        if G.has_edge(w, x):
            continue
        e = frozenset([u, v])
        other_canon = edge_to_other.get(e)
        if other_canon is None or other_canon == outer_canon:
            continue
        other_depth = depths.get(other_canon)
        if other_depth is None:
            continue
        if best is None or other_depth < best[4]:
            best = (u, v, w, x, other_depth)
    return best


# -- main routine -------------------------------------------------------

def analyze_config(G, emb, levels, nodes_k, k, max_steps=20):
    """Run the user's algorithm iteratively. Returns:
       {'terminated': bool, 'steps': n, 'trace': list of monovariant tuples}
    A trace entry per step is (max_tricky_depth, num_tricky, max_odd_depth).
    """
    Gc = G.copy()
    emb_c = emb
    trace = []
    for step in range(max_steps):
        faces = faces_of_subgraph(Gc, emb_c, nodes_k)
        if len(faces) < 2:
            return {'terminated': True, 'steps': step, 'trace': trace,
                    'reason': 'no_faces'}
        outer = identify_outer_face(faces, Gc, emb_c, levels, k)
        depths = face_depths(faces, outer)
        if depths is None:
            return {'terminated': True, 'steps': step, 'trace': trace,
                    'reason': 'no_depths'}
        odd_all = odd_faces_all(faces, depths, outer)
        tricky = tricky_odd_faces(faces, Gc, emb_c, levels, k, depths, outer)
        max_tricky = max((d for _, _, d in tricky), default=-1)
        max_odd = max((d for _, _, d in odd_all), default=-1)
        trace.append((max_tricky, len(tricky), max_odd))
        if not tricky:
            return {'terminated': True, 'steps': step, 'trace': trace,
                    'reason': 'no_tricky'}
        # Pick deepest tricky face; flip toward lowest-depth neighbor.
        cf_t, face_t, d_t = max(tricky, key=lambda x: x[2])
        flip = lowest_depth_neighbor_flip(face_t, Gc, emb_c, levels, k,
                                           depths, faces, outer)
        if flip is None:
            return {'terminated': False, 'steps': step, 'trace': trace,
                    'reason': '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 {'terminated': False, 'steps': step, 'trace': trace,
                    'reason': 'nonplanar'}
    return {'terminated': False, 'steps': max_steps, 'trace': trace,
            'reason': 'budget'}


def monovariant_decreases(trace, key):
    """Does `trace[i][key]` strictly decrease at each step (except when
    the algorithm has already terminated)?"""
    vals = [t[key] for t in trace]
    for i in range(1, len(vals)):
        if vals[i] >= vals[i - 1]:
            return False
    return True


def run_check(n_values, max_steps=20):
    total_configs = 0
    terminated = 0
    nonterm = 0
    mono_max_tricky = 0
    mono_count_tricky = 0
    mono_max_odd = 0
    nonterm_examples = []

    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)
                for k, nodes_k in by_level.items():
                    if k == 0:
                        continue
                    faces = faces_of_subgraph(G, emb, nodes_k)
                    if len(faces) < 2:
                        continue
                    outer = identify_outer_face(faces, G, emb, levels, k)
                    if outer is None:
                        continue
                    depths = face_depths(faces, outer)
                    if depths is None:
                        continue
                    tricky = tricky_odd_faces(faces, G, emb, levels, k,
                                              depths, outer)
                    if not tricky:
                        continue
                    total_configs += 1
                    result = analyze_config(G, emb, levels, nodes_k, k,
                                             max_steps)
                    if result['terminated']:
                        terminated += 1
                    else:
                        nonterm += 1
                        if len(nonterm_examples) < 5:
                            nonterm_examples.append({
                                'n': n, 'tri_idx': tri_idx,
                                'source': (kind, label),
                                'k': k,
                                'reason': result['reason'],
                                'trace': result['trace'],
                                'steps': result['steps'],
                            })
                    # Monovariant check (only meaningful if trace has >= 2)
                    tr = result['trace']
                    if len(tr) >= 2:
                        if monovariant_decreases(tr, 0):
                            mono_max_tricky += 1
                        if monovariant_decreases(tr, 1):
                            mono_count_tricky += 1
                        if monovariant_decreases(tr, 2):
                            mono_max_odd += 1

    print(f"\n=== summary ===")
    print(f"tricky configs encountered:        {total_configs}")
    print(f"  terminated within {max_steps} steps:    {terminated}")
    print(f"  non-terminating / failed:        {nonterm}")
    print(f"  candidate monovariants (strict decrease per step):")
    print(f"    max depth of tricky faces:     {mono_max_tricky}")
    print(f"    count of tricky faces:         {mono_count_tricky}")
    print(f"    max depth of all odd faces:    {mono_max_odd}")
    if nonterm_examples:
        print(f"\nnon-terminating examples (first {len(nonterm_examples)}):")
        for ex in nonterm_examples:
            print(f"  n={ex['n']} tri={ex['tri_idx']} src={ex['source']} "
                  f"k={ex['k']} reason={ex['reason']} steps={ex['steps']}")
            print(f"    trace (max_tricky, n_tricky, max_odd): {ex['trace']}")


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