"""Fast exhaustive bridge-derivability decision for a dual.

For each valid parity partition L, exhaust the backward bridge-orbit
(integer states) and look for an ELG witness with BFS-parity L. The ELG
check is gated by a cheap filter: an ELG source s lies in one parity
class and ALL its neighbours lie in the other (they are level 1), so only
such candidate sources are fully tested.

YES (bridge-derived) as soon as any partition yields a witness; NO
(conclusive) if all partitions' orbits are fully exhausted with no
witness; INCONCLUSIVE if any orbit hits the cap.
"""
import sys
import os
import time
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
                'level_resolutions_of_maximal_planar_graphs/experiments')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import networkx as nx
from collections import deque
from load_holton_mckay import parse_planar_code
from tutte_dual_treecolor import dual_triangulation
from exhaustive_bridge import valid_parity_partitions
from fast_bridge import EdgeCode, parity_bridges
from test_conjecture import is_even_level_graph


def _bfs_parity(adj, s, n):
    """BFS distance parity from s over adjacency dict; returns dict v->parity
    or None if graph (restricted to reached) doesn't cover all n nodes."""
    par = {s: 0}
    dq = deque([s])
    while dq:
        u = dq.popleft()
        for w in adj[u]:
            if w not in par:
                par[w] = par[u] ^ 1
                dq.append(w)
    if len(par) != n:
        return None
    return par


def expand_and_check(state, code, labels, n):
    """Build G once; return (is_witness, list_of_neighbor_states)."""
    G = code.graph_of(state)
    adj = {v: set(G.neighbors(v)) for v in code.nodes}
    # --- ELG witness check (gated) ---
    # Necessary condition for BFS-parity(s)==L: s's whole neighbourhood is
    # the opposite class (level 1). Only such s are fully tested with the
    # ground-truth ELG check (all level subgraphs bipartite) + parity match.
    witness = False
    for s in code.nodes:
        cs = labels[s]
        nbrs = adj[s]
        if not nbrs or any(labels[w] == cs for w in nbrs):
            continue
        ok, lvls = is_even_level_graph(G, frozenset({s}))
        if not ok:
            continue
        if all((lvls[v] % 2 == 0) == (labels[v] == cs) for v in code.nodes):
            witness = True
            break
    # --- neighbours (backward bridge switches) ---
    neigh = []
    ok, emb = nx.check_planarity(G)
    if ok:
        even_adj = {v: set() for v in code.nodes if labels[v] == 0}
        odd_adj = {v: set() for v in code.nodes if labels[v] == 1}
        for u in code.nodes:
            for v in adj[u]:
                if u < v and labels[u] == labels[v]:
                    if labels[u] == 0:
                        even_adj[u].add(v); even_adj[v].add(u)
                    else:
                        odd_adj[u].add(v); odd_adj[v].add(u)
        bridges = parity_bridges(even_adj) | parity_bridges(odd_adj)
        for u, v in G.edges():
            f1 = emb.traverse_face(u, v)
            if len(f1) != 3:
                continue
            f2 = emb.traverse_face(v, u)
            if len(f2) != 3:
                continue
            w = next(a for a in f1 if a != u and a != v)
            x = next(b for b in f2 if b != u and b != v)
            if w == x or G.has_edge(w, x):
                continue
            if labels[w] != labels[x]:
                continue
            if labels[u] == labels[v] and frozenset((u, v)) not in bridges:
                continue
            ns = (state & ~(1 << code.bit(u, v))) | (1 << code.bit(w, x))
            neigh.append(ns)
    return witness, neigh


def decide_partition(code, labels, n, cap):
    s0 = code.state0
    seen = {s0}
    frontier = [s0]
    while frontier and len(seen) < cap:
        new = []
        for st in frontier:
            wit, neigh = expand_and_check(st, code, labels, n)
            if wit:
                return 'found', len(seen)
            for ns in neigh:
                if ns not in seen:
                    seen.add(ns)
                    new.append(ns)
        frontier = new
    return ('exhausted' if not frontier else 'capped'), len(seen)


def decide_dual(i, cap=4_000_000, log=print):
    graphs = parse_planar_code('experiments/nonham38m4.pc')
    G, _ = dual_triangulation(graphs[i][0])
    n = G.number_of_nodes()
    code = EdgeCode(G.nodes())
    code.state0 = code.state_of(G)
    parts = list(valid_parity_partitions(G))
    log(f'dual {i}: {len(parts)} partitions, n={n}', flush=True)
    t0 = time.time()
    any_capped = False
    max_orbit = 0
    for j, labels in enumerate(parts):
        st, sz = decide_partition(code, labels, n, cap)
        if st == 'found':
            log(f'  partition {j}: WITNESS -> dual {i} IS bridge-derived '
                f'(orbit>={sz}, {time.time()-t0:.0f}s)', flush=True)
            return 'bridge-derived'
        if st == 'capped':
            any_capped = True
            log(f'  partition {j}: CAP {sz} (inconclusive)', flush=True)
        else:
            max_orbit = max(max_orbit, sz)
        if (j + 1) % 10 == 0:
            log(f'  ...{j+1}/{len(parts)} done, max_orbit={max_orbit}, '
                f'{time.time()-t0:.0f}s', flush=True)
    verdict = 'INCONCLUSIVE (cap hit)' if any_capped else 'NOT bridge-derived'
    log(f'  dual {i}: {verdict} (max_orbit={max_orbit}, {time.time()-t0:.0f}s)',
        flush=True)
    return verdict


if __name__ == '__main__':
    for idx in sys.argv[1:]:
        decide_dual(int(idx))
