class CSP:
    def __init__(self, variables, domains, constraints):
        self.variables = variables
        self.domains = {v: set(domains[v]) for v in variables}
        self.constraints = constraints  # var -> list of (other_var, fn)

    def is_consistent(self, var, value, assignment):
        for other, fn in self.constraints.get(var, []):
            if other in assignment and not fn(value, assignment[other]):
                return False
        return True


def mrv_select(assignment, csp):
    unassigned = [v for v in csp.variables if v not in assignment]
    return min(unassigned, key=lambda v: len(csp.domains[v]))


def backtrack_fc(assignment, csp, domains):
    if len(assignment) == len(csp.variables):
        return dict(assignment)

    var = mrv_select(assignment, csp)
    for value in list(domains[var]):
        if not csp.is_consistent(var, value, assignment):
            continue
        assignment[var] = value
        saved = {}
        wiped_out = False
        for other, fn in csp.constraints.get(var, []):
            if other in assignment:
                continue
            saved[other] = set(domains[other])
            domains[other] = {
                v for v in domains[other] if fn(v, value)
            }
            if not domains[other]:
                wiped_out = True
                break
        if not wiped_out:
            result = backtrack_fc(assignment, csp, domains)
            if result is not None:
                return result
        for other, vals in saved.items():
            domains[other] = vals
        del assignment[var]
    return None


def n_queens_csp(n):
    variables = list(range(n))  # variables[i] = column of queen in row i

    def not_attacking(col_a, col_b):
        # constraint closure captures row offset via default args
        return True  # placeholder; replaced per-pair below

    constraints = {v: [] for v in variables}
    for i in variables:
        for j in variables:
            if i == j:
                continue

            def make_fn(row_i=i, row_j=j):
                def fn(col_i, col_j):
                    return (
                        col_i != col_j
                        and abs(col_i - col_j) != abs(row_i - row_j)
                    )
                return fn

            constraints[i].append((j, make_fn()))

    domains = {v: list(range(n)) for v in variables}
    return CSP(variables, domains, constraints)


if __name__ == "__main__":
    csp = n_queens_csp(8)
    init_domains = {v: set(csp.domains[v]) for v in csp.variables}
    solution = backtrack_fc({}, csp, init_domains)
    print("8-Queens solution (row -> column):", solution)
