#!/usr/bin/env python3
import lzma, glob, itertools, os, sys, collections

def read(path):
    opener = lzma.open if path.endswith('.xz') else open
    clauses=[]; n=0
    with opener(path,'rt') as f:
        for line in f:
            if not line.strip() or line.startswith('c'): continue
            if line.startswith('p'):
                n=int(line.split()[2]); continue
            lits=[int(x) for x in line.split() if x!='0']
            if lits: clauses.append(lits)
    return n, clauses

def is_pos(c): return c and all(x>0 for x in c)
def is_neg2(c): return len(c)==2 and c[0]<0 and c[1]<0
def is_mixed2(c): return len(c)==2 and c[0]<0 and c[1]>0

def parse_seq(clauses, i, edgevars, d):
    need = 3*d-4
    if i+need>len(clauses): return None
    # d=4 pattern: [-x1 s1],[-x2 s2],[-x2 -s1],[-s1 s2],[-x3 s3],[-x3 -s2],[-s2 s3],[-x4 -s3]
    # generalized for d>=2
    xs=[]; ss=[]; idx=i
    c=clauses[idx]
    if not (len(c)==2 and c[0]<0 and c[1]>0 and abs(c[0]) in edgevars and c[1] not in edgevars): return None
    xs.append(abs(c[0])); ss.append(c[1]); idx+=1
    for t in range(2,d):
        c1=clauses[idx]; c2=clauses[idx+1]; c3=clauses[idx+2]
        if not (len(c1)==2 and c1[0]<0 and c1[1]>0 and abs(c1[0]) in edgevars and c1[1] not in edgevars): return None
        x=abs(c1[0]); s=c1[1]
        if not (len(c2)==2 and c2[0]<0 and c2[1]<0 and abs(c2[0])==x and abs(c2[1])==ss[-1]): return None
        if not (len(c3)==2 and c3[0]<0 and c3[1]>0 and abs(c3[0])==ss[-1] and c3[1]==s): return None
        xs.append(x); ss.append(s); idx+=3
    c=clauses[idx]
    if not (len(c)==2 and c[0]<0 and c[1]<0 and abs(c[0]) in edgevars and abs(c[1])==ss[-1]): return None
    xs.append(abs(c[0])); idx+=1
    if len(set(xs))!=d or len(set(ss))!=d-1: return None
    return xs, idx

def parse_clique(clauses, i, edgevars, k):
    need=k*(k-1)//2
    if i+need>len(clauses): return None
    pairs=[]; vs=set()
    for j in range(need):
        c=clauses[i+j]
        if not is_neg2(c): return None
        a,b=abs(c[0]),abs(c[1])
        if a not in edgevars or b not in edgevars or a==b: return None
        pairs.append(tuple(sorted((a,b)))); vs.update([a,b])
    if len(vs)!=k: return None
    if set(pairs) != set(itertools.combinations(sorted(vs),2)): return None
    return sorted(vs), i+need

def parse_mixed(clauses):
    pos=[]; i=0
    while i<len(clauses) and is_pos(clauses[i]):
        pos.append((i+1, clauses[i])); i+=1
    edgevars=set(v for _,c in pos for v in c)
    groups=[]; used=collections.Counter(); start=i
    while i<len(clauses) and len(used)<len(edgevars):
        parsed=None; kind=None
        for d in (4,3,2):
            r=parse_seq(clauses,i,edgevars,d) if d>=2 else None
            if r:
                parsed, ni = r; kind=f'seq{d}'; break
        if parsed is None:
            for k in (4,3,2):
                r=parse_clique(clauses,i,edgevars,k)
                if r:
                    parsed, ni = r; kind=f'clique{k}'; break
        if parsed is None:
            print('stopped at', i+1, 'clause', clauses[i])
            break
        groups.append((kind, parsed, i+1, ni))
        for v in parsed: used[v]+=1
        i=ni
        # If all positive edge vars are covered exactly once, stop before extra constraints.
        if len(used)==len(edgevars) and all(used[v]==1 for v in edgevars):
            break
    return pos, groups, used, i

def parse_plain(clauses):
    pos=[(i+1,c) for i,c in enumerate(clauses) if is_pos(c)]
    inc=collections.defaultdict(list)
    for idx,c in pos:
        for v in c: inc[v].append(idx)
    # build graph among positive clause ids for vars that appear in two clauses
    adj=collections.defaultdict(list)
    for v,ids in inc.items():
        if len(ids)==2:
            a,b=ids; adj[a].append((b,v)); adj[b].append((a,v))
    color={}; ok=True
    for idx,_ in pos:
        if idx in color: continue
        color[idx]=0; stack=[idx]
        while stack:
            u=stack.pop()
            for w,_ in adj[u]:
                if w not in color:
                    color[w]=color[u]^1; stack.append(w)
                elif color[w]==color[u]: ok=False
    cnt=collections.Counter(color.values())
    return pos, inc, color, cnt, ok

for path in sys.argv[1:] or sorted(glob.glob('/data/train/*.cnf.xz')):
    n,clauses=read(path)
    mixed=any(len(c)==2 and c[0]<0 and c[1]>0 for c in clauses)
    print('\n',os.path.basename(path), 'n', n, 'clauses', len(clauses), 'mixed', mixed)
    if mixed:
        pos, groups, used, stop=parse_mixed(clauses)
        print('pos', len(pos), collections.Counter(len(c) for _,c in pos), 'edgevars', len({v for _,c in pos for v in c}))
        print('groups', len(groups), collections.Counter(k for k,_,_,_ in groups), collections.Counter(len(g) for _,g,_,_ in groups), 'covered', len(used), 'badcover', sum(1 for v,c in used.items() if c!=1), 'stop line', stop+1, 'extra', len(clauses)-stop)
        missing=set(v for _,c in pos for v in c)-set(used)
        print('missing', len(missing), 'dups', [v for v,c in used.items() if c!=1][:10])
    else:
        pos, inc, color, cnt, ok=parse_plain(clauses)
        print('pos', len(pos), collections.Counter(len(c) for _,c in pos), 'bip ok', ok, 'colors', cnt)
        print('var inc hist', collections.Counter(len(ids) for ids in inc.values()))
