"""
binary_service.py
=================
Core binary tree placement logic for the Hybrid Global Assisted Binary system.

Responsibilities:
  1. assign_binary_position  — place a new DistributorID in the correct binary slot.
  2. BFS spillover           — when both direct slots of a sponsor are filled, walk
                               down the sponsor's subtree BFS-style to find the first
                               empty LEFT or RIGHT slot.
  3. backfill_binary_tree    — replay all existing sponsor relationships into binary
                               placement (called from management command).
  4. get_binary_subtree_ids  — return the flat list of all node IDs under a root.
  5. count_binary_subtree    — fast count of all nodes under a root.
  6. Integrity helpers.

Design notes:
  • The sponsor tree (sponsor_distributor) records WHO introduced a member.
  • The binary tree (binary_parent_distributor + binary_position) records WHERE
    in the 2-slot binary structure they sit.
  • These two trees are distinct; spillover means the binary parent may differ
    from the sponsor.
  • For SQLite (dev) we use Python BFS. For PostgreSQL (prod) the same logic
    works; add WITH RECURSIVE CTEs in a future optimisation pass.
"""

import logging
from collections import deque

from django.db import transaction

from apps.business.distributor.models import DistributorID, distributor_runtime_guard
from apps.business.binary_tree.models import BinaryPlacementLog

logger = logging.getLogger(__name__)

LEFT = DistributorID.BinaryPosition.LEFT
RIGHT = DistributorID.BinaryPosition.RIGHT


def _compose_binary_path(parent, position):
    parent_path = (getattr(parent, 'binary_path', '') or '').strip('.')
    if not parent_path:
        return position
    return f"{parent_path}.{position}"


# ── helpers ──────────────────────────────────────────────────────────────────

def _get_left_child(node_id):
    return DistributorID.objects.filter(
        binary_parent_distributor_id=node_id, binary_position=LEFT
    ).values_list('id', flat=True).first()


def _get_right_child(node_id):
    return DistributorID.objects.filter(
        binary_parent_distributor_id=node_id, binary_position=RIGHT
    ).values_list('id', flat=True).first()


def _children_ids(node_id):
    """Return (left_id_or_None, right_id_or_None)."""
    children = dict(
        DistributorID.objects.filter(binary_parent_distributor_id=node_id)
        .values_list('binary_position', 'id')
    )
    return children.get(LEFT), children.get(RIGHT)


# ── BFS to find the first open binary slot under a root ─────────────────────

def find_first_open_slot(root_distributor_id):
    """
    BFS through the binary subtree rooted at root_distributor_id.
    Returns (parent_id, position) where the next node should be placed,
    or raises ValueError if the tree is completely full (very unlikely in practice).

    LEFT is always preferred over RIGHT (breadth-first, left-to-right).
    """
    queue = deque([root_distributor_id])
    visited = set()

    while queue:
        current_id = queue.popleft()
        if current_id in visited:
            continue
        visited.add(current_id)

        left_id, right_id = _children_ids(current_id)

        if left_id is None:
            return current_id, LEFT
        if right_id is None:
            return current_id, RIGHT

        queue.append(left_id)
        queue.append(right_id)

    raise ValueError(
        f"No open binary slot found under distributor {root_distributor_id}. "
        "This should not happen in a properly maintained binary tree."
    )


# ── Assign binary position to a new DistributorID ───────────────────────────

@transaction.atomic
def assign_binary_position(new_distributor, sponsor_distributor):
    """
    Legacy sponsor-based placement entry point.

    The strict Hybrid Global Assisted Binary architecture forbids sponsor-driven
    binary seating. This function remains only to fail closed if an old runtime
    path is invoked accidentally.
    """
    message = (
        f"Legacy sponsor-based binary placement is disabled for distributor {new_distributor.id}. "
        "Use create_distributor_id() and company-root BFS placement only."
    )
    logger.warning(message)
    raise RuntimeError(message)


@transaction.atomic
def assign_company_global_binary_position(new_distributor, company_root):
    """
    Place a distributor in the company-managed global binary tree using BFS.

    Placement is independent of sponsor tree and always under the company root subtree.
    """
    if not company_root:
        logger.info(
            "[BINARY_PLACEMENT] skipped_no_root distributor=%s",
            new_distributor.pk,
        )
        return {'placed': False, 'reason': 'no_company_root'}

    new_dist = DistributorID.objects.select_for_update().get(pk=new_distributor.pk)
    if new_dist.binary_parent_distributor_id is not None:
        logger.info(
            "[BINARY_PLACEMENT] already_placed distributor=%s parent=%s position=%s",
            new_dist.pk,
            new_dist.binary_parent_distributor_id,
            new_dist.binary_position,
        )
        return {'placed': False, 'reason': 'already_placed'}

    if new_dist.pk == company_root.pk:
        with distributor_runtime_guard('assign_company_global_binary_position:root'):
            DistributorID.objects.filter(pk=new_dist.pk).update(
                binary_parent_distributor=None,
                binary_position=None,
                binary_level_depth=0,
                binary_path='ROOT',
                is_company_root=True,
            )
        logger.info("[BINARY_PLACEMENT] root_assigned distributor=%s", new_dist.pk)
        return {'placed': True, 'reason': 'root'}

    parent_id, position = find_first_open_slot(company_root.pk)
    parent = DistributorID.objects.only('id', 'binary_level_depth', 'binary_path').get(pk=parent_id)

    with distributor_runtime_guard('assign_company_global_binary_position:update'):
        DistributorID.objects.filter(pk=new_dist.pk).update(
            binary_parent_distributor_id=parent_id,
            binary_position=position,
            binary_level_depth=(parent.binary_level_depth + 1),
            binary_path=_compose_binary_path(parent, position),
        )

    logger.info(
        "[BINARY_PLACEMENT] placed distributor=%s parent=%s position=%s depth=%s",
        new_dist.pk,
        parent_id,
        position,
        parent.binary_level_depth + 1,
    )

    BinaryPlacementLog.objects.create(
        distributor_id=new_dist.pk,
        binary_parent_id=parent_id,
        binary_position=position,
        placement_type=BinaryPlacementLog.PlacementType.SPILLOVER,
        sponsor_id=getattr(new_dist, 'sponsor_distributor_id', None),
        notes=f"Company global BFS placement under root={company_root.pk}",
    )

    return {
        'placed': True,
        'parent_id': str(parent_id),
        'position': position,
        'placement_type': BinaryPlacementLog.PlacementType.SPILLOVER,
    }


# ── Subtree utilities ────────────────────────────────────────────────────────

def get_binary_subtree_ids(root_id, max_nodes=100_000):
    """
    BFS collect all descendant IDs in the binary subtree rooted at root_id.
    Does NOT include root_id itself.
    Stops at max_nodes to protect against unbounded traversal.

    Uses chunked DB queries to avoid large IN clauses.
    """
    all_ids = []
    frontier = [root_id]
    seen = {root_id}

    while frontier and len(all_ids) < max_nodes:
        chunk_size = 500
        children = []
        for i in range(0, len(frontier), chunk_size):
            batch = frontier[i: i + chunk_size]
            rows = list(
                DistributorID.objects.filter(binary_parent_distributor_id__in=batch)
                .values_list('id', flat=True)
            )
            children.extend(rows)

        next_frontier = []
        for cid in children:
            if cid not in seen:
                seen.add(cid)
                all_ids.append(cid)
                next_frontier.append(cid)

        frontier = next_frontier

    return all_ids


def count_binary_subtree(root_id):
    """Efficient count of all descendants in the binary subtree (excludes root)."""
    return len(get_binary_subtree_ids(root_id))


def get_binary_ancestor_ids(distributor_id, max_depth=50):
    """Return ancestor distributor IDs following binary_parent links only."""
    ancestor_ids = []
    visited = set()
    current_id = getattr(distributor_id, 'binary_parent_distributor_id', None)

    while current_id and current_id not in visited and len(ancestor_ids) < max_depth:
        visited.add(current_id)
        ancestor_ids.append(current_id)
        current_id = (
            DistributorID.objects.filter(pk=current_id)
            .values_list('binary_parent_distributor_id', flat=True)
            .first()
        )

    return ancestor_ids


def get_binary_left_right_volumes(root_id):
    """
    Return (left_volume, right_volume) where:
      left_volume  = total nodes in the LEFT  subtree of root
      right_volume = total nodes in the RIGHT subtree of root
    """
    left_child_id, right_child_id = _children_ids(root_id)
    left_vol = (1 + count_binary_subtree(left_child_id)) if left_child_id else 0
    right_vol = (1 + count_binary_subtree(right_child_id)) if right_child_id else 0
    return left_vol, right_vol


def get_binary_tree_depth(root_id, max_depth=50):
    """BFS to calculate the maximum depth of the binary subtree."""
    if not root_id:
        return 0
    depth = 0
    current_level = [root_id]
    while current_level and depth < max_depth:
        next_level = list(
            DistributorID.objects.filter(binary_parent_distributor_id__in=current_level)
            .values_list('id', flat=True)
        )
        if not next_level:
            break
        depth += 1
        current_level = next_level
    return depth


def build_binary_subtree_dict(root_id, max_depth=10):
    """
    Build a nested dict representation of the binary tree for API serialization.
    Limited to max_depth to keep responses manageable.

    Returns:
      {
        "id": "...",
        "distributor_code": "...",
        "user_name": "...",
        "is_active": bool,
        "binary_position": "LEFT"|"RIGHT"|null,
        "left": {...} | null,
        "right": {...} | null,
        "depth": int,
      }
    """
    if not root_id or max_depth <= 0:
        return None

    try:
        node = DistributorID.objects.select_related('user').get(pk=root_id)
    except DistributorID.DoesNotExist:
        return None

    left_child_id, right_child_id = _children_ids(root_id)

    return {
        'id': str(node.id),
        'distributor_code': node.distributor_code,
        'global_position': node.global_position,
        'user_name': (
            f"{getattr(node.user, 'first_name', '') or ''} "
            f"{getattr(node.user, 'last_name', '') or ''}".strip()
            or str(node.user)
        ),
        'user_id': str(node.user_id),
        'referral_code': getattr(node.user, 'referral_code', None),
        'is_active': node.is_active,
        'binary_position': node.binary_position,
        # has_left/has_right tell the UI whether a slot is truly empty
        # vs just not loaded due to depth limit
        'has_left': left_child_id is not None,
        'has_right': right_child_id is not None,
        'left': build_binary_subtree_dict(left_child_id, max_depth - 1),
        'right': build_binary_subtree_dict(right_child_id, max_depth - 1),
    }


# ── Backfill for existing data ───────────────────────────────────────────────

def _find_open_slot_in_memory(tree, root_id):
    """
    Pure-Python BFS on the in-memory tree dict.
    Returns (parent_id, 'LEFT'|'RIGHT') or (None, None) if no slot found.
    """
    queue = deque([root_id])
    visited = set()
    while queue:
        cur = queue.popleft()
        if cur in visited:
            continue
        visited.add(cur)
        slots = tree.get(cur)
        if slots is None:
            continue
        if slots['LEFT'] is None:
            return cur, 'LEFT'
        if slots['RIGHT'] is None:
            return cur, 'RIGHT'
        queue.append(slots['LEFT'])
        queue.append(slots['RIGHT'])
    return None, None


def backfill_binary_tree(bundle_id=None, dry_run=False):
    """
    O(n) bulk backfill using a single GLOBAL BFS queue.

    The binary tree is one global structure. Nodes are placed in BFS order
    (level-by-level, left-to-right) based on their global_position sequence.
    The sponsor field tracks recruitment/commission relationships but does NOT
    affect binary placement.

    Algorithm:
      • Sort nodes by global_position (join order).
      • The first node becomes the binary tree root (no parent needed).
      • Maintain a single global FIFO deque of open (parent_id, position) slots.
        – Initialize with the root's two child slots.
        – For each subsequent node: pop the front slot, record the placement,
          push the node's own two child slots.
      • Nodes with no sponsor (or no global_position) become additional roots
        whose child slots are also added to the global queue.
      • Bulk-write all (node_id, parent_id, position) tuples in one transaction.

    This guarantees zero slot collisions (each (parent, position) pair used once)
    and runs in O(n) time with O(n) space.
    """
    qs = DistributorID.objects.order_by('global_position')
    if bundle_id:
        qs = qs.filter(product__opportunity_bundle_id=bundle_id)

    # ── 1. Clear any partial placements from a previous run ──────────────────
    if not dry_run:
        scope_qs = DistributorID.objects.all()
        if bundle_id:
            scope_qs = scope_qs.filter(product__opportunity_bundle_id=bundle_id)
        with distributor_runtime_guard('backfill_binary_tree:reset'):
            scope_qs.update(
                binary_parent_distributor_id=None,
                binary_position=None,
                binary_level_depth=0,
                binary_path='',
                is_company_root=False,
            )
        if bundle_id:
            BinaryPlacementLog.objects.filter(
                distributor__product__opportunity_bundle_id=bundle_id
            ).delete()
        else:
            BinaryPlacementLog.objects.all().delete()

    # ── 2. Load all rows in global_position order ─────────────────────────────
    rows = list(qs.values('id', 'sponsor_distributor_id'))
    if not rows:
        return {'placed': 0, 'already_placed': 0, 'skipped_no_sponsor': 0,
                'errors': 0, 'dry_run': dry_run}

    # ── 3. Single global BFS queue ────────────────────────────────────────────
    global_queue: deque = deque()

    # First node is the global root — no binary parent assigned
    root_id = rows[0]['id']
    if not dry_run:
        with distributor_runtime_guard('backfill_binary_tree:root'):
            DistributorID.objects.filter(pk=root_id).update(
                binary_parent_distributor_id=None,
                binary_position=None,
                binary_level_depth=0,
                binary_path='ROOT',
                is_company_root=True,
            )
    global_queue.append((root_id, 'LEFT', 0, 'ROOT'))
    global_queue.append((root_id, 'RIGHT', 0, 'ROOT'))
    root_nodes = 1  # count of nodes that became roots (no binary parent)

    placed = 0
    errors = 0
    to_update = []

    for row in rows[1:]:
        node_id = row['id']

        if not global_queue:
            # Should never happen in a proper binary tree
            logger.error("[BACKFILL] Global queue empty — cannot place %s", node_id)
            errors += 1
            continue

        parent_id, position, parent_depth, parent_path = global_queue.popleft()
        node_depth = parent_depth + 1
        node_path = f"{parent_path}.{position}" if parent_path else position
        if not dry_run:
            to_update.append((node_id, parent_id, position, node_depth, node_path))

        # This node opens two new child slots at the back (BFS order)
        global_queue.append((node_id, 'LEFT', node_depth, node_path))
        global_queue.append((node_id, 'RIGHT', node_depth, node_path))
        placed += 1

    # ── 4. Bulk-write ─────────────────────────────────────────────────────────
    if not dry_run and to_update:
        _bulk_write_placements(to_update)

    return {
        'placed': placed,
        'already_placed': 0,
        'skipped_no_sponsor': root_nodes,   # root nodes have no binary parent
        'errors': errors,
        'dry_run': dry_run,
    }


def _bulk_write_placements(to_update, batch_size=2000):
    """
    Write binary placements as fast as possible.

    Uses raw SQL executemany inside a single transaction for the DistributorID
    updates — this is orders-of-magnitude faster than Django bulk_update for
    SQLite (which generates one large CASE-WHEN query per batch).

    IMPORTANT: SQLite (and MySQL) store UUIDs as 32-char hex WITHOUT dashes.
    Use uuid.hex instead of str(uuid) when building raw SQL params.
    PostgreSQL stores native UUID so str(uuid) works there.

    BinaryPlacementLog records are written with bulk_create in batches.
    """
    import uuid as _uuid
    from django.db import connection

    table = DistributorID._meta.db_table  # 'distributor_distributorid'

    vendor = connection.vendor  # 'sqlite', 'mysql', 'postgresql'
    ph = '%s' if vendor != 'sqlite' else '?'

    def _to_db_uuid(v):
        """Convert uuid.UUID (or string UUID) to the format the current DB vendor expects."""
        if isinstance(v, _uuid.UUID):
            return str(v) if vendor == 'postgresql' else v.hex
        # Already a string — normalize to correct format
        try:
            u = _uuid.UUID(str(v))
            return str(u) if vendor == 'postgresql' else u.hex
        except (ValueError, AttributeError):
            return v

    sql = (
        f'UPDATE "{table}" '
        f'SET binary_parent_distributor_id = {ph}, binary_position = {ph}, '
        f'binary_level_depth = {ph}, binary_path = {ph} '
        f'WHERE id = {ph}'
    )
    params = [
        (_to_db_uuid(parent_id), position, depth, path, _to_db_uuid(node_id))
        for node_id, parent_id, position, depth, path in to_update
    ]

    # Single transaction → single fsync in SQLite WAL mode
    with transaction.atomic():
        with connection.cursor() as cursor:
            cursor.executemany(sql, params)

    # Bulk-create audit logs (ignore if duplicates from a prior partial run)
    logs = [
        BinaryPlacementLog(
            distributor_id=node_id,
            binary_parent_id=parent_id,
            binary_position=position,
            placement_type=BinaryPlacementLog.PlacementType.SPILLOVER,
            notes='Backfill',
        )
        for node_id, parent_id, position, _depth, _path in to_update
    ]
    for i in range(0, len(logs), batch_size):
        BinaryPlacementLog.objects.bulk_create(
            logs[i: i + batch_size], batch_size=batch_size, ignore_conflicts=True,
        )
    logger.info("[BACKFILL] Wrote %d placements via executemany.", len(to_update))


# ── Integrity checks ─────────────────────────────────────────────────────────

def detect_binary_cycles(distributor_id, max_depth=200):
    """
    Walk up the binary_parent chain from distributor_id.
    Return list of visited IDs. If a cycle is detected, the repeated ID is
    appended at the end of the list and the function returns early.
    """
    visited = []
    current_id = distributor_id
    while current_id and len(visited) < max_depth:
        if current_id in visited:
            visited.append(current_id)  # marks the cycle
            return visited, True
        visited.append(current_id)
        current_id = (
            DistributorID.objects.filter(pk=current_id)
            .values_list('binary_parent_distributor_id', flat=True)
            .first()
        )
    return visited, False


def verify_binary_integrity(bundle_id=None):
    """
    O(n) bulk scan of strict company-binary integrity.

    Returns a summary report with structural failures plus informational sponsor
    vs binary comparisons for audit/reporting purposes.
    """
    from collections import defaultdict

    qs = DistributorID.objects.all()
    if bundle_id:
        qs = qs.filter(product__opportunity_bundle_id=bundle_id)

    rows = list(qs.values(
        'id',
        'global_position',
        'product__opportunity_bundle_id',
        'binary_parent_distributor_id',
        'binary_position',
        'sponsor_distributor_id',
        'is_company_root',
    ))
    checked = len(rows)
    invalid_positions = []
    duplicate_child_slots = []
    orphan_nodes = []
    missing_parent_references = []
    sponsor_binary_mismatches = []
    bfs_inconsistencies = []
    unreachable_nodes = []
    multiple_roots = []

    slot_map = {}
    rows_by_bundle = defaultdict(list)
    row_by_id = {}
    for row in rows:
        row_by_id[row['id']] = row
        rows_by_bundle[row['product__opportunity_bundle_id']].append(row)
        if row['binary_parent_distributor_id'] is not None and row['binary_position'] not in ('LEFT', 'RIGHT'):
            invalid_positions.append(str(row['id']))

        if row['sponsor_distributor_id'] and row['binary_parent_distributor_id'] != row['sponsor_distributor_id']:
            sponsor_binary_mismatches.append({
                'distributor': str(row['id']),
                'sponsor': str(row['sponsor_distributor_id']),
                'binary_parent': str(row['binary_parent_distributor_id']) if row['binary_parent_distributor_id'] else None,
            })

        pid = row['binary_parent_distributor_id']
        pos = row['binary_position']
        if pid is None:
            continue

        if pid not in row_by_id:
            missing_parent_references.append({
                'distributor': str(row['id']),
                'missing_parent': str(pid),
            })
            continue

        if pos not in ('LEFT', 'RIGHT'):
            continue

        slot_key = (pid, pos)
        if slot_key in slot_map:
            duplicate_child_slots.append({
                'distributor': str(row['id']),
                'parent': str(pid),
                'position': pos,
                'conflict_with': str(slot_map[slot_key]),
            })
        else:
            slot_map[slot_key] = row['id']

    parent_map = {row['id']: row['binary_parent_distributor_id'] for row in rows}
    id_set = set(parent_map.keys())
    visited = set()
    cycles = []
    for start in id_set:
        if start in visited:
            continue
        path = []
        path_set = set()
        node = start
        while node is not None and node in id_set and node not in visited:
            if node in path_set:
                cycles.append(str(node))
                break
            path.append(node)
            path_set.add(node)
            node = parent_map.get(node)
        visited.update(path)

    for current_bundle_id, bundle_rows in rows_by_bundle.items():
        bundle_rows = sorted(bundle_rows, key=lambda item: (item['global_position'] or 0, str(item['id'])))
        roots = [row for row in bundle_rows if row['binary_parent_distributor_id'] is None]
        if len(roots) != 1:
            multiple_roots.append({
                'bundle': str(current_bundle_id) if current_bundle_id else None,
                'root_count': len(roots),
                'roots': [str(row['id']) for row in roots],
            })

        for row in bundle_rows:
            if row['binary_parent_distributor_id'] is None:
                continue
            if row['binary_parent_distributor_id'] not in id_set:
                continue
            if row['binary_position'] not in ('LEFT', 'RIGHT'):
                continue
        if not roots:
            continue

        root = roots[0]
        if bundle_rows[0]['id'] != root['id']:
            bfs_inconsistencies.append({
                'distributor': str(root['id']),
                'expected_parent': None,
                'expected_position': None,
                'actual_parent': str(root['binary_parent_distributor_id']) if root['binary_parent_distributor_id'] else None,
                'actual_position': root['binary_position'],
                'reason': 'root_not_first_by_global_position',
            })

        children_by_parent = defaultdict(dict)
        for row in bundle_rows:
            pid = row['binary_parent_distributor_id']
            pos = row['binary_position']
            if pid and pos in ('LEFT', 'RIGHT'):
                children_by_parent[pid][pos] = row['id']

        reachable = {root['id']}
        queue = deque([root['id']])
        while queue:
            current_id = queue.popleft()
            for child_id in children_by_parent.get(current_id, {}).values():
                if child_id in reachable:
                    continue
                reachable.add(child_id)
                queue.append(child_id)

        unreachable_nodes.extend(
            str(row['id'])
            for row in bundle_rows
            if row['id'] not in reachable
        )

        expected_queue = deque([(root['id'], LEFT), (root['id'], RIGHT)])
        for row in bundle_rows[1:]:
            if not expected_queue:
                bfs_inconsistencies.append({
                    'distributor': str(row['id']),
                    'expected_parent': None,
                    'expected_position': None,
                    'actual_parent': str(row['binary_parent_distributor_id']) if row['binary_parent_distributor_id'] else None,
                    'actual_position': row['binary_position'],
                    'reason': 'unexpected_extra_node',
                })
                continue

            expected_parent, expected_position = expected_queue.popleft()
            actual_parent = row['binary_parent_distributor_id']
            actual_position = row['binary_position']
            if actual_parent is None:
                orphan_nodes.append(str(row['id']))
                bfs_inconsistencies.append({
                    'distributor': str(row['id']),
                    'expected_parent': str(expected_parent),
                    'expected_position': expected_position,
                    'actual_parent': None,
                    'actual_position': actual_position,
                    'reason': 'missing_binary_parent',
                })
                continue
            if (actual_parent, actual_position) != (expected_parent, expected_position):
                bfs_inconsistencies.append({
                    'distributor': str(row['id']),
                    'expected_parent': str(expected_parent),
                    'expected_position': expected_position,
                    'actual_parent': str(actual_parent),
                    'actual_position': actual_position,
                    'reason': 'global_bfs_mismatch',
                })

            expected_queue.append((row['id'], LEFT))
            expected_queue.append((row['id'], RIGHT))

    subtree_corruption = {
        'missing_parent_references': missing_parent_references,
        'unreachable_nodes': sorted(set(unreachable_nodes)),
    }

    return {
        'checked': checked,
        'total_checked': checked,
        'cycles': cycles,
        'orphan_nodes': sorted(set(orphan_nodes)),
        'orphan_mismatches': duplicate_child_slots,
        'duplicate_child_slots': duplicate_child_slots,
        'invalid_positions': invalid_positions,
        'missing_parent_references': missing_parent_references,
        'multiple_roots': multiple_roots,
        'bfs_inconsistencies': bfs_inconsistencies,
        'subtree_corruption': subtree_corruption,
        'sponsor_binary_mismatches': sponsor_binary_mismatches,
        'unplaced_with_sponsor': sorted({
            str(row['id'])
            for row in rows
            if row['binary_parent_distributor_id'] is None and row['sponsor_distributor_id'] is not None
        }),
        'issues_found': bool(
            cycles or duplicate_child_slots or invalid_positions or orphan_nodes
            or missing_parent_references or multiple_roots or bfs_inconsistencies
            or subtree_corruption['unreachable_nodes']
        ),
    }
