import numpy as np

def one_hot_encode(ops_idx, num_classes=5):
    ops_idx = np.array(ops_idx, dtype=int)
    one_hot = np.eye(num_classes)[ops_idx]
    return one_hot

def reachable_from_start(adj, start=0):
    import collections
    n = adj.shape[0]
    visited = [False] * n
    visited[start] = True
    
    queue = collections.deque([start])
    while queue:
        u = queue.popleft()
        for v in range(n):
            if not visited[v] and adj[u, v] != 0:
                visited[v] = True
                queue.append(v)
    return {i for i, val in enumerate(visited) if val}

def reverse_graph(adj):
    import numpy as np
    n = adj.shape[0]
    rev = np.zeros((n,n), dtype=adj.dtype)
    for i in range(n):
        for j in range(n):
            if adj[i, j] != 0:
                rev[j, i] = adj[i, j]
    return rev

def can_reach_end(adj, end=6):
    rev = reverse_graph(adj)
    return reachable_from_start(rev, start=end)

def largest_subgraph_0_to_6(adj):
    r0 = reachable_from_start(adj, start=0)
    r6 = can_reach_end(adj, end=6)
    S = r0.intersection(r6)
    
    if len(S) == 0:
        return np.zeros((0,0), dtype=adj.dtype), []
    
    sorted_nodes = sorted(S)
    k = len(sorted_nodes)
    sub_matrix = np.zeros((k, k), dtype=adj.dtype)
    
    for i, old_i in enumerate(sorted_nodes):
        for j, old_j in enumerate(sorted_nodes):
            if i < j:
                sub_matrix[i,j] = adj[old_i, old_j]
            else:
                sub_matrix[i,j] = 0
    
    return sub_matrix, sorted_nodes

import random

def generate_random_adj_matrix(n, p=0.5):
    adj = np.zeros((n, n), dtype=np.float32)
    for i in range(n):
        for j in range(i+1, n):
            adj[i, j] = 1 if random.random() < p else 0

    submat, _ = largest_subgraph_0_to_6(adj)
    return submat


def all_topological_sorts(adj: np.ndarray) -> list:
    n = adj.shape[0]
    indeg = np.sum(adj, axis=0).astype(int)

    all_orders = []
    
    def backtrack(current_order, current_indeg):
        if len(current_order) == n:
            all_orders.append(list(current_order))
            return
        
        zero_indeg_nodes = [i for i in range(n) if current_indeg[i] == 0 and i not in current_order]
        
        for node in zero_indeg_nodes:
            current_order.append(node)
            
            modified_indeg = current_indeg.copy()

            for child in range(n):
                if adj[node, child] != 0:
                    modified_indeg[child] -= 1
            
            backtrack(current_order, modified_indeg)
            
            current_order.pop()
    
    backtrack([], indeg)
    return all_orders

import hashlib

def get_spec_hash(adj: np.ndarray, ops_idx: np.ndarray) -> str:
    flat_adj = adj.flatten().astype(int)  # shape=(n*n,)

    flat_ops = ops_idx.astype(int)        # shape=(n,)

    adj_str = ''.join(str(x) for x in flat_adj)
    ops_str = ''.join(str(x) for x in flat_ops)
    data_str = adj_str + ops_str 

    data_bytes = data_str.encode('utf-8')
    hash_val = hashlib.sha256(data_bytes).hexdigest()
    return hash_val

def reorder_graph(adj: np.ndarray, ops: np.ndarray, order: list):
    reordered_adj = adj[order, :][:, order]

    reordered_ops = ops[order]
    return reordered_adj, reordered_ops