#!/usr/bin/env python3
import lzma
import math
import os
import sys
from collections import Counter


def read_cnf(path):
    opener = lzma.open if path.endswith(".xz") else open
    nvars = nclauses = None
    clauses = []
    with opener(path, "rt") as f:
        for line in f:
            if not line or line[0] == "c":
                continue
            if line[0] == "p":
                parts = line.split()
                nvars = int(parts[2])
                nclauses = int(parts[3])
                continue
            lits = [int(x) for x in line.split()]
            if lits and lits[-1] == 0:
                lits.pop()
            clauses.append(lits)
    assert nvars is not None
    assert nclauses == len(clauses), (nclauses, len(clauses))
    return nvars, clauses


def pair_list(n):
    return [(i, j) for i in range(n) for j in range(i + 1, n)]


def derive_params(nvars, clauses):
    units = [c[0] for c in clauses if len(c) == 1]
    n = int(round(math.sqrt(len(units))))
    if n * n != len(units):
        raise ValueError(f"unit count {len(units)} is not square")
    pstart = min(abs(x) for c in clauses for x in c)
    C = n * (n - 1) // 2
    used = nvars - pstart + 1
    k_num = used - 3 * n * C
    if k_num % (n * n):
        raise ValueError((nvars, pstart, n, C, used, k_num))
    k = k_num // (n * n)
    return n, k, pstart


def var_maps(n, k, pstart):
    C = n * (n - 1) // 2
    pairs = pair_list(n)
    pidx = {p: a for a, p in enumerate(pairs)}
    for a, (i, j) in enumerate(pairs):
        pidx[(j, i)] = a

    qstart = pstart + n * C
    rstart = qstart + n * C
    sstart = rstart + n * n
    lstart = sstart + n * C

    def P(t, i, j):
        if i == j:
            return None
        if i > j:
            i, j = j, i
        return pstart + t * C + pidx[(i, j)]

    def Q(t, i, j):
        if i == j:
            return None
        if i > j:
            i, j = j, i
        return qstart + t * C + pidx[(i, j)]

    def R(t, v):
        return rstart + t * n + v

    def S(t, i, j):
        if i > j:
            i, j = j, i
        return sstart + t * C + pidx[(i, j)]

    def L(t, v, a):
        # a in 0..k-2
        return lstart + (t * n + v) * (k - 1) + a

    return pairs, P, Q, R, S, L


def recover_edges(n, k, pstart, clauses):
    C = n * (n - 1) // 2
    pairs = pair_list(n)
    # The first non-unit binary block contains edge constraints Q_t(edge)->P_{t-1}(edge).
    edges = set()
    qstart = pstart + n * C
    for c in clauses:
        if len(c) != 2:
            continue
        a, b = c
        if a > 0 and b < 0 and pstart <= a < pstart + (n - 1) * C:
            offp = a - pstart
            t = offp // C
            idx = offp % C
            want_q = qstart + (t + 1) * C + idx
            if -b == want_q:
                edges.add(pairs[idx])
        # Stop after the first q=>p refinement block starts.
        if len(edges) and a == pstart and b == -qstart:
            break
    return edges


def find_order(n, k, edges):
    adj = [0] * n
    for u, v in edges:
        adj[u] |= 1 << v
        adj[v] |= 1 << u

    # Backtracking over suffix. Score of appending v at position t is number of
    # Q groups in the new prefix: old-prefix vertices grouped by neighborhoods
    # into the future suffix, plus the newly appended singleton.
    full = (1 << n) - 1
    memo = {}
    parent = {}

    def group_count(prefix, v):
        new_prefix = prefix | (1 << v)
        suffix = full ^ prefix
        types = set()
        x = prefix
        while x:
            lb = x & -x
            u = lb.bit_length() - 1
            types.add(adj[u] & suffix)
            x -= lb
        return len(types) + 1

    def dfs(prefix):
        t = prefix.bit_count()
        if t == n:
            return True
        key = prefix
        if key in memo:
            return memo[key]
        rem = [v for v in range(n) if not (prefix >> v) & 1]
        # Prefer vertices that keep few old-neighborhood types and have extreme degree.
        scored = []
        for v in rem:
            gc = 1 if t == 0 else group_count(prefix, v)
            scored.append((gc, -((adj[v] & ~prefix).bit_count()), v))
        scored.sort()
        for gc, _, v in scored:
            if t > 0 and gc > k:
                continue
            np = prefix | (1 << v)
            if dfs(np):
                parent[prefix] = v
                memo[key] = True
                return True
        memo[key] = False
        return False

    # Try every first vertex; memo is compatible because first singleton has no
    # label-width cost.
    if not dfs(0):
        return None
    order = []
    p = 0
    while p.bit_count() < n:
        v = parent[p]
        order.append(v)
        p |= 1 << v
    return order


def build_assignment(nvars, n, k, pstart, edges, order):
    pairs, P, Q, R, S, L = var_maps(n, k, pstart)
    C = len(pairs)
    adj = [[False] * n for _ in range(n)]
    for u, v in edges:
        adj[u][v] = adj[v][u] = True

    pos_of = {v: i for i, v in enumerate(order)}
    q_group = [[None] * n for _ in range(n)]

    val = [False] * (nvars + 1)
    # Unused prefix stays false.

    for t in range(n):
        prefix = {order[i] for i in range(t + 1)}
        old = {order[i] for i in range(t)}
        suffix_for_old = [order[i] for i in range(t, n)]
        # P: one growing prefix component plus outside singletons.
        for i, j in pairs:
            val[P(t, i, j)] = i in prefix and j in prefix

        # Q: old prefix grouped by identical future neighborhoods; new vertex is singleton.
        type_to_gid = {}
        next_gid = 0
        for v in old:
            typ = tuple(1 if adj[v][w] else 0 for w in suffix_for_old)
            if typ not in type_to_gid:
                type_to_gid[typ] = next_gid
                next_gid += 1
            q_group[t][v] = type_to_gid[typ]
        if t >= 0:
            q_group[t][order[t]] = next_gid
            next_gid += 1
        for i in range(t + 1, n):
            v = order[i]
            if q_group[t][v] is None:
                q_group[t][v] = next_gid
                next_gid += 1

        for i, j in pairs:
            val[Q(t, i, j)] = q_group[t][i] == q_group[t][j]

        # R: representative (smallest vertex index) for each Q block.
        for v in range(n):
            rep = True
            for u in range(v):
                if q_group[t][u] == q_group[t][v]:
                    rep = False
                    break
            val[R(t, v)] = rep

        # S: two Q-block representatives inside the same P component.
        for i, j in pairs:
            val[S(t, i, j)] = val[P(t, i, j)] and val[R(t, i)] and val[R(t, j)]

        # L: order code for representative rank inside each P component.
        # Clauses for S(i,j), i<j, enforce label(i) > label(j) with
        # L(v,a) meaning label(v) <= a.
        comps = {}
        for v in range(n):
            if v in prefix:
                cid = 0
            else:
                cid = v + 1
            if val[R(t, v)]:
                comps.setdefault(cid, []).append(v)
        label = [0] * n
        for reps in comps.values():
            reps.sort()
            m = len(reps)
            for a, v in enumerate(reps):
                label[v] = max(0, min(k - 1, m - 1 - a))
        for v in range(n):
            for a in range(k - 1):
                val[L(t, v, a)] = label[v] <= a

    return val


def check(clauses, val):
    bad = []
    for ci, c in enumerate(clauses, 1):
        ok = False
        for lit in c:
            b = val[abs(lit)]
            if lit < 0:
                b = not b
            if b:
                ok = True
                break
        if not ok:
            bad.append((ci, c))
            if len(bad) >= 10:
                break
    return bad


def main():
    for path in sys.argv[1:]:
        nvars, clauses = read_cnf(path)
        n, k, pstart = derive_params(nvars, clauses)
        edges = recover_edges(n, k, pstart, clauses)
        order = find_order(n, k, edges)
        print(os.path.basename(path), "n", n, "k", k, "edges", len(edges), "order", order)
        if order is None:
            continue
        val = build_assignment(nvars, n, k, pstart, edges, order)
        bad = check(clauses, val)
        print("  check", "OK" if not bad else bad[:3])


if __name__ == "__main__":
    main()
