"""Diagram generation for Azure VNet topology using Draw.io XML format."""

from __future__ import annotations

import logging
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom import minidom

from gettopology.models import (
    TopologyModel,
    VirtualNetworkModel,
    extract_subscription_id_from_id,
)
from gettopology.diagram.config import get_config
from gettopology.diagram.models import VNetCategory, HubSpokeGroup
from gettopology.diagram.categorization import categorize_vnets, group_hubs_with_spokes
from gettopology.diagram.layout import (
    calculate_zone_positions,
    calculate_spoke_positions,
    calculate_hubless_spoke_positions,
    calculate_external_zone_positions,
)
from gettopology.diagram.elements.vnet_elements import (
    calculate_vnet_height,
    create_vnet_group,
    create_external_vnet_group,
    extract_resource_group_from_id,
)
from gettopology.diagram.legend import create_legend
from gettopology.diagram.elements.edge_elements import build_edge_style
from gettopology.diagram.connections.hub_to_hub import create_hub_to_hub_connection
from gettopology.diagram.connections.hub_to_spoke import create_hub_to_spoke_connection

# Get global config instance
_config = get_config()

# Draw.io layout constants (from config)
CANVAS_PADDING = _config.canvas_padding
ZONE_WIDTH = _config.zone_width
ZONE_SPACING = _config.zone_spacing
VNET_WIDTH = _config.vnet_width
VNET_HEIGHT = _config.vnet_height_base
VNET_SPACING_X = _config.vnet_spacing_x
VNET_SPACING_Y = _config.vnet_spacing_y
HUB_Y = _config.hub_y
SPOKE_START_Y = _config.spoke_start_y
SPACING_BELOW_HUB = _config.spacing_below_hub
SPACING_BETWEEN_SPOKES = _config.spacing_between_spokes
SUBNET_PADDING_TOP = _config.subnet_padding_top
SUBNET_HEIGHT = _config.subnet_height
SUBNET_SPACING = _config.subnet_spacing
VNET_MIN_HEIGHT = _config.vnet_min_height
EXTERNAL_SPACING_X = _config.external_spacing_x
EXTERNAL_SPACING_Y = _config.external_spacing_y
STUB_SPACING_X = _config.stub_spacing_x
EDGE_COLOR_HUBLESS_SPOKE = _config.edge_color_hubless_spoke
EDGE_COLOR_CROSS_TENANT = _config.edge_color_cross_tenant

# VNet height calculation is now in vnet_elements module
# Imported above as calculate_vnet_height


# Models and categorization functions are now in separate modules
# Imported above


def _create_peering_edge(
    root: Element,
    cell_id: int,
    source_id: int,
    target_id: int,
    is_local: bool = True,
    source_is_hub: bool = False,
    target_is_hub: bool = False,
    source_x: float | None = None,
    target_x: float | None = None,
    source_y: float | None = None,
    target_y: float | None = None,
    source_height: float | None = None,
    target_height: float | None = None,
    source_name: str = "",
    target_name: str = "",
    hubless_spoke_names: set[str] | None = None,
    hubless_center_name: str | None = None,  # Deprecated: use hubless_group_map and hubless_centers
    hubless_group_map: dict[str, int] | None = None,  # vnet_name -> group_index
    hubless_centers: dict[int, str] | None = None,  # group_index -> center_name
    is_cross_tenant: bool = False,
    hub_positions: dict[str, tuple[float, float, float]] | None = None,
    all_vnet_positions: dict[str, tuple[float, float, float]] | None = None,
 ) -> int:
    """Create a peering connection edge mxCell element.
    
    Args:
        root: Root element to add the cell to
        cell_id: Starting cell ID (will be incremented)
        source_id: Source VNet cell ID
        target_id: Target VNet cell ID
        is_local: True if local peering (same subscription), False if cross-subscription
        source_is_hub: Whether source VNet is a hub
        target_is_hub: Whether target VNet is a hub
        
    Returns:
        The cell ID used for this edge
    """
    # Determine edge style based on peering type:
    # - Hub-to-Hub: Dark purple solid (#6A1B9A)
    # - Hub-to-Spoke: Blue solid (#0078D4) for local, Green solid (#28A745) for cross-subscription
    # - Spoke-to-Spoke: Dashed maroon/red (#8B0000) for local, Green solid for cross-subscription
    # - Cross-subscription: Green solid (NOT dashed) (#28A745)
    # - External (horizontal): Green solid with straight line style
    
    # Check if this is an external connection (external VNet above hub or hubless spoke)
    # Use lenient tolerance (VNET_WIDTH) to catch hubless spoke connections
    from gettopology.diagram.connections.external import is_external_connection as check_external_connection
    is_external_connection = False
    if source_x is not None and target_x is not None and source_y is not None and target_y is not None and source_height is not None and target_height is not None:
        source_center_x = source_x + (VNET_WIDTH / 2)
        target_center_x = target_x + (VNET_WIDTH / 2)
        target_bottom_y = target_y + target_height
        source_top_y = source_y
        # Use lenient tolerance (VNET_WIDTH) to catch hubless spoke connections
        is_external_connection = target_bottom_y < source_top_y and abs(target_center_x - source_center_x) < VNET_WIDTH
    
    if source_is_hub and target_is_hub:
        # Hub-to-Hub: Dark purple with configurable pattern
        pattern = _config.edge_pattern_hub_to_hub
        edge_style = build_edge_style(_config.color_hub_to_hub, pattern, "none", _config)
    elif is_external_connection or is_cross_tenant:
        # External (cross-tenant) or cross-tenant connection: Use gray color from config
        if is_external_connection and source_is_hub:
            # External connection from hub: straight vertical line
            edge_style = f"edgeStyle=none;rounded=0;html=1;strokeColor={EDGE_COLOR_CROSS_TENANT};strokeWidth=2;startArrow=block;endArrow=block;"
        else:
            # External from spoke or cross-tenant: use orthogonal routing to avoid overlapping boxes
            edge_style = f"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor={EDGE_COLOR_CROSS_TENANT};strokeWidth=2;startArrow=block;endArrow=block;"
    elif (source_is_hub or target_is_hub) and not (hubless_spoke_names and (source_name in hubless_spoke_names or target_name in hubless_spoke_names)):
        # Hub-to-Spoke (real hub, not hubless): Blue with configurable pattern
        pattern = _config.edge_pattern_hub_to_spoke
        edge_style = build_edge_style(_config.color_hub_to_spoke, pattern, "orthogonalEdgeStyle", _config)
    elif hubless_spoke_names and (source_name in hubless_spoke_names or target_name in hubless_spoke_names):
        # Spoke-to-Spoke (hubless): Use maroon for same tenant, gray for cross-tenant
        if is_cross_tenant:
            # Cross-tenant hubless spoke connection: gray
            edge_style = f"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor={EDGE_COLOR_CROSS_TENANT};strokeWidth=2;startArrow=block;endArrow=block;"
        else:
            # Same tenant hubless spoke connection: maroon with configurable pattern
            pattern = _config.edge_pattern_hubless_spoke
            edge_style = build_edge_style(EDGE_COLOR_HUBLESS_SPOKE, pattern, "orthogonalEdgeStyle", _config)
    else:
        # Fallback: Spoke-to-Spoke (hubless): Maroon with configurable pattern
        pattern = _config.edge_pattern_hubless_spoke
        edge_style = build_edge_style(EDGE_COLOR_HUBLESS_SPOKE, pattern, "orthogonalEdgeStyle", _config)
    
    # Create mxCell for edge
    mxCell = SubElement(root, "mxCell")
    mxCell.set("id", str(cell_id))
    mxCell.set("style", edge_style)
    mxCell.set("edge", "1")
    mxCell.set("parent", "1")
    mxCell.set("source", str(source_id))
    mxCell.set("target", str(target_id))
    
    # Add geometry with explicit connection points for hub-to-spoke connections
    mxGeometry = SubElement(mxCell, "mxGeometry")
    mxGeometry.set("relative", "1")
    mxGeometry.set("as", "geometry")
    
    # For Hub-to-Spoke connections: Hub connects from bottom center, Spoke connects to appropriate side
    # For Hub-to-External connections: Hub connects from top middle, External connects to bottom middle
    # Check if this is an external connection first (before checking hub-to-spoke)
    if is_external_connection:
        # Use dedicated module for external connections
        from gettopology.diagram.connections.external import create_external_connection
        create_external_connection(
            root, mxGeometry,
            source_x, target_x,
            source_y, target_y,
            source_height, target_height,
            source_name, source_is_hub,
            hubless_centers, hubless_group_map,
            hub_positions, all_vnet_positions
        )
    elif not source_is_hub and not target_is_hub and source_x is not None and target_x is not None and source_y is not None and target_y is not None and source_height is not None and target_height is not None and all_vnet_positions:
        # Spoke-to-Spoke connection: Always connect to side centers, route around blocking VNets
        source_center_x = source_x + (VNET_WIDTH / 2)
        source_center_y = source_y + (source_height / 2)
        target_center_x = target_x + (VNET_WIDTH / 2)
        target_center_y = target_y + (target_height / 2)
        
        # DEBUG LOGGING
        logging.debug(f"[CONNECTION DEBUG] Spoke-to-Spoke: {source_name} -> {target_name}")
        logging.debug(f"  source_x={source_x}, target_x={target_x}")
        logging.debug(f"  hubless_center_name={hubless_center_name}")
        logging.debug(f"  source in hubless_spoke_names: {source_name in (hubless_spoke_names or set())}")
        logging.debug(f"  target in hubless_spoke_names: {target_name in (hubless_spoke_names or set())}")
        logging.debug(f"  source == hubless_center_name: {source_name == hubless_center_name}")
        logging.debug(f"  target == hubless_center_name: {target_name == hubless_center_name}")
        
        # Determine which side to connect on (inner vs outer) based on hub/center position
        # Inner sides: facing the hub/center (for spokes on same side)
        # Outer sides: facing away from hub/center (for spokes on opposite sides)
        
        # Find hub/center position for source and target
        source_hub_x = None
        target_hub_x = None
        
        # Check if source is a hubless spoke (has hubless center)
        if hubless_group_map and hubless_centers:
            source_group = hubless_group_map.get(source_name)
            if source_group is None:
                # Check if source is a center
                for group_idx, center_name in hubless_centers.items():
                    if source_name == center_name:
                        source_group = group_idx
                        break
            if source_group is not None:
                center_name = hubless_centers.get(source_group)
                if center_name:
            # Try to find hubless center position from hub_positions or all_vnet_positions
                    if hub_positions and center_name in hub_positions:
                        source_hub_x, _, _ = hub_positions[center_name]
                        logging.debug(f"  Found source_hub_x={source_hub_x} from hub_positions (group {source_group})")
                    elif all_vnet_positions and center_name in all_vnet_positions:
                        source_hub_x, _, _ = all_vnet_positions[center_name]
                        logging.debug(f"  Found source_hub_x={source_hub_x} from all_vnet_positions (group {source_group})")
                if source_name == center_name:
                    # Source IS the hubless center
                    source_hub_x = source_x
                    logging.debug(f"  Source IS hubless center (group {source_group}), source_hub_x={source_hub_x}")
        elif hubless_center_name and hubless_spoke_names and source_name in hubless_spoke_names:
            # Fallback to old logic
            if hub_positions and hubless_center_name in hub_positions:
                source_hub_x, _, _ = hub_positions[hubless_center_name]
                logging.debug(f"  Found source_hub_x={source_hub_x} from hub_positions (fallback)")
            elif all_vnet_positions and hubless_center_name in all_vnet_positions:
                source_hub_x, _, _ = all_vnet_positions[hubless_center_name]
                logging.debug(f"  Found source_hub_x={source_hub_x} from all_vnet_positions (fallback)")
        elif source_name == hubless_center_name:
            # Source IS the hubless center (fallback)
            source_hub_x = source_x
            logging.debug(f"  Source IS hubless center, source_hub_x={source_hub_x} (fallback)")
        
        # Check if target is a hubless spoke (has hubless center)
        if hubless_group_map and hubless_centers:
            target_group = hubless_group_map.get(target_name)
            if target_group is None:
                # Check if target is a center
                for group_idx, center_name in hubless_centers.items():
                    if target_name == center_name:
                        target_group = group_idx
                        break
            if target_group is not None:
                center_name = hubless_centers.get(target_group)
                if center_name:
            # Try to find hubless center position from hub_positions or all_vnet_positions
                    if hub_positions and center_name in hub_positions:
                        target_hub_x, _, _ = hub_positions[center_name]
                        logging.debug(f"  Found target_hub_x={target_hub_x} from hub_positions (group {target_group})")
                    elif all_vnet_positions and center_name in all_vnet_positions:
                        target_hub_x, _, _ = all_vnet_positions[center_name]
                        logging.debug(f"  Found target_hub_x={target_hub_x} from all_vnet_positions (group {target_group})")
                if target_name == center_name:
                    # Target IS the hubless center
                    target_hub_x = target_x
                    logging.debug(f"  Target IS hubless center (group {target_group}), target_hub_x={target_hub_x}")
        elif hubless_center_name and hubless_spoke_names and target_name in hubless_spoke_names:
            # Fallback to old logic
            if hub_positions and hubless_center_name in hub_positions:
                target_hub_x, _, _ = hub_positions[hubless_center_name]
                logging.debug(f"  Found target_hub_x={target_hub_x} from hub_positions (fallback)")
            elif all_vnet_positions and hubless_center_name in all_vnet_positions:
                target_hub_x, _, _ = all_vnet_positions[hubless_center_name]
                logging.debug(f"  Found target_hub_x={target_hub_x} from all_vnet_positions (fallback)")
        elif target_name == hubless_center_name:
            # Target IS the hubless center (fallback)
            target_hub_x = target_x
            logging.debug(f"  Target IS hubless center, target_hub_x={target_hub_x} (fallback)")
        
        # For regular hub-spoke, find the hub by checking which hub the spoke peers with
        # Find closest hub (spoke should be near its hub)
        if source_hub_x is None and hub_positions:
            source_center_x = source_x + (VNET_WIDTH / 2)
            min_distance = float('inf')
            for hub_name, (hub_x, _, _) in hub_positions.items():
                hub_center_x = hub_x + (VNET_WIDTH / 2)
                distance = abs(source_center_x - hub_center_x)
                if distance < min_distance:
                    min_distance = distance
                    source_hub_x = hub_x
        
        if target_hub_x is None and hub_positions:
            target_center_x = target_x + (VNET_WIDTH / 2)
            min_distance = float('inf')
            for hub_name, (hub_x, _, _) in hub_positions.items():
                hub_center_x = hub_x + (VNET_WIDTH / 2)
                distance = abs(target_center_x - hub_center_x)
                if distance < min_distance:
                    min_distance = distance
                    target_hub_x = hub_x
        
        # Determine if spokes are on same side or opposite sides
        source_is_left = False
        target_is_left = False
        
        source_center_x = source_x + (VNET_WIDTH / 2)
        target_center_x = target_x + (VNET_WIDTH / 2)
        
        if source_hub_x is not None:
            source_hub_center_x = source_hub_x + (VNET_WIDTH / 2)
            source_is_left = source_center_x < source_hub_center_x
            logging.debug(f"  source_is_left={source_is_left} (source_center_x={source_center_x} < source_hub_center_x={source_hub_center_x})")
            if source_is_left:
                logging.debug(f"  → {source_name} is LEFT spoke (to the left of center)")
            else:
                logging.debug(f"  → {source_name} is RIGHT spoke (to the right of center)")
        else:
            # If we can't find hub, try to infer from position relative to target
            # If source is to the left of target and they're close, source might be left spoke
            if source_center_x < target_center_x and abs(source_center_x - target_center_x) < (VNET_WIDTH * 3):
                source_is_left = True
            logging.debug(f"  source_is_left={source_is_left} (inferred, source_hub_x is None)")
            if source_is_left:
                logging.debug(f"  → {source_name} is LEFT spoke (inferred)")
            else:
                logging.debug(f"  → {source_name} is RIGHT spoke (inferred)")
        
        if target_hub_x is not None:
            target_hub_center_x = target_hub_x + (VNET_WIDTH / 2)
            target_is_left = target_center_x < target_hub_center_x
            logging.debug(f"  target_is_left={target_is_left} (target_center_x={target_center_x} < target_hub_center_x={target_hub_center_x})")
            if target_is_left:
                logging.debug(f"  → {target_name} is LEFT spoke (to the left of center)")
            else:
                logging.debug(f"  → {target_name} is RIGHT spoke (to the right of center)")
        else:
            # If we can't find hub, try to infer from position relative to source
            # If target is to the right of source and they're close, target might be right spoke
            if target_center_x > source_center_x and abs(source_center_x - target_center_x) < (VNET_WIDTH * 3):
                target_is_left = False
            elif target_center_x < source_center_x and abs(source_center_x - target_center_x) < (VNET_WIDTH * 3):
                target_is_left = True
            logging.debug(f"  target_is_left={target_is_left} (inferred, target_hub_x is None)")
            if target_is_left:
                logging.debug(f"  → {target_name} is LEFT spoke (inferred)")
            else:
                logging.debug(f"  → {target_name} is RIGHT spoke (inferred)")
        
        # Determine connection sides
        # Spoke-to-spoke connections:
        # - Same hub/group: use inner sides (facing hub/center)
        # - Different hub/group: use outer sides (facing away from their hubs, towards each other)
        # Exception: connections to/from central hub/spoke are handled separately
        
        # Check if this is a hubless spoke connection (both must be in the same hubless group)
        is_hubless_connection = False
        if hubless_group_map and hubless_centers:
            # Check if both VNets are hubless spokes and in the same group
            source_group = hubless_group_map.get(source_name)
            target_group = hubless_group_map.get(target_name)
            # Also check if source or target is a hubless center
            if source_group is None:
                # Check if source is a center
                for group_idx, center_name in hubless_centers.items():
                    if source_name == center_name:
                        source_group = group_idx
                        break
            if target_group is None:
                # Check if target is a center
                for group_idx, center_name in hubless_centers.items():
                    if target_name == center_name:
                        target_group = group_idx
                        break
            
            if source_group is not None and target_group is not None and source_group == target_group:
                is_hubless_connection = True
                logging.debug(f"  is_hubless_connection=True (hubless group {source_group} connection)")
        elif hubless_center_name and hubless_spoke_names:
            # Fallback to old logic for backward compatibility
            if (source_name in hubless_spoke_names or source_name == hubless_center_name) and \
               (target_name in hubless_spoke_names or target_name == hubless_center_name):
                is_hubless_connection = True
                logging.debug(f"  is_hubless_connection=True (hubless group connection - fallback)")
        
        # Check if spokes belong to the same hub (for regular hub-spoke groups)
        same_hub = False
        if not is_hubless_connection:
            # Only check same_hub for regular hub-spoke connections
            if source_hub_x is not None and target_hub_x is not None:
                same_hub = (source_hub_x == target_hub_x)
                logging.debug(f"  same_hub={same_hub} (compared hub_x: {source_hub_x} == {target_hub_x})")
            elif source_hub_x is None and target_hub_x is None:
                # Neither has a hub found - check if they're on opposite sides (left vs right)
                # This suggests they might be in the same hub group but hub detection failed
                # Use a heuristic: if one is clearly left and one is clearly right, and they're close horizontally,
                # they're likely in the same hub group
                if source_is_left != target_is_left:
                    # Opposite sides - check if they're reasonably close (within 3x VNET_WIDTH)
                    horizontal_distance = abs(source_center_x - target_center_x)
                    if horizontal_distance < (VNET_WIDTH * 3):
                        same_hub = True  # Likely same hub group
                        logging.debug(f"  same_hub={same_hub} (heuristic: opposite sides, distance={horizontal_distance})")
            else:
                # One has hub_x, the other doesn't - check if they're on opposite sides and close
                if source_is_left != target_is_left:
                    horizontal_distance = abs(source_center_x - target_center_x)
                    if horizontal_distance < (VNET_WIDTH * 3):
                        same_hub = True  # Likely same hub group
                        logging.debug(f"  same_hub={same_hub} (heuristic: one hub_x found, opposite sides, distance={horizontal_distance})")
        
        # Check if this is a Central Spoke → Spoke connection (should use Hub → Spoke pattern, not layout-aware rules)
        is_central_spoke_to_spoke = False
        if is_hubless_connection and hubless_centers:
            source_group = hubless_group_map.get(source_name) if hubless_group_map else None
            target_group = hubless_group_map.get(target_name) if hubless_group_map else None
            
            # Check if source is center
            if source_group is None:
                for group_idx, center_name in hubless_centers.items():
                    if source_name == center_name:
                        source_group = group_idx
                        break
            
            # Check if target is center
            if target_group is None:
                for group_idx, center_name in hubless_centers.items():
                    if target_name == center_name:
                        target_group = group_idx
                        break
            
            # If one is center and the other is not, this is Central Spoke → Spoke
            if source_group is not None and target_group is not None and source_group == target_group:
                source_center_name = hubless_centers.get(source_group)
                target_center_name = hubless_centers.get(target_group)
                is_central_spoke_to_spoke = (source_name == source_center_name and target_name != target_center_name) or \
                                           (target_name == target_center_name and source_name != source_center_name)
        
        # For hubless connections, apply special rules based on layout
        # For regular hub-spoke, use same_hub logic to determine if layout-aware rules apply
        # Layout-aware rules (same-side vertical/outer, cross-side inner-to-inner) apply to:
        # 1. Hubless Spoke ↔ Spoke connections (NOT Central Spoke → Spoke)
        # 2. Same-hub connections (spokes within the same hub group)
        # Central Spoke → Spoke uses Hub → Spoke pattern (handled separately below)
        use_layout_aware_rules = (is_hubless_connection and not is_central_spoke_to_spoke) or same_hub
        logging.debug(f"  FINAL: is_hubless_connection={is_hubless_connection}, is_central_spoke_to_spoke={is_central_spoke_to_spoke}, same_hub={same_hub}, use_layout_aware_rules={use_layout_aware_rules}")
        
        if use_layout_aware_rules:
            # LAYOUT-AWARE CONNECTION LOGIC (applies to hubless and same-hub connections)
            # Use dedicated module for hubless/same-hub connections
            from gettopology.diagram.connections.hubless import create_hubless_connection
            
            connection_type = "HUBLESS" if is_hubless_connection else "SAME-HUB"
            logging.debug(f"  *** CONNECTION TYPE: {connection_type} (layout-aware rules) ***")
            
            create_hubless_connection(
                root, mxGeometry,
                source_x, target_x,
                source_y, target_y,
                source_height, target_height,
                source_name, target_name,
                source_is_left, target_is_left,
                source_center_y, target_center_y,
                all_vnet_positions,
                same_hub
            )
            
            # Connection points and waypoints are set by create_hubless_connection
            # Skip blocking detection and waypoint routing for layout-aware connections
        else:
            # Different hub/group: use outer sides (facing away from their hubs, towards each other)
            # Determine which outer sides face each other
            if target_x < source_x:
                # Target is to the left: source connects from left side (outer), target connects to right side (outer)
                source_exit_x = 0.0  # Left edge (outer)
                target_entry_x = 1.0  # Right edge (outer)
            else:
                # Target is to the right: source connects from right side (outer), target connects to left side (outer)
                source_exit_x = 1.0  # Right edge (outer)
                target_entry_x = 0.0  # Left edge (outer)
            logging.debug(f"  Different hub: source_exit_x={source_exit_x}, target_entry_x={target_entry_x}")
            
            # Set connection points for different-hub connections
            mxGeometry.set("exitX", str(source_exit_x))
            mxGeometry.set("exitY", "0.5")  # Side connection: use 0.5 for vertical center
            mxGeometry.set("exitDx", "0")
            mxGeometry.set("exitDy", "0")
            mxGeometry.set("exitPerimeter", "1")
            
            mxGeometry.set("entryX", str(target_entry_x))
            mxGeometry.set("entryY", "0.5")  # Side connection: use 0.5 for vertical center
            mxGeometry.set("entryDx", "0")
            mxGeometry.set("entryDy", "0")
            mxGeometry.set("entryPerimeter", "1")
        
        # Blocking detection: Only for different-hub connections
        # Layout-aware connections (hubless and same-hub) follow specific rules
        # (same-side vertical/outer, different-side inner-to-inner) so blocking detection is not needed
        vnet_in_path = False
        blocking_vnet_x = None
        blocking_vnet_y = None
        blocking_vnet_height = None
        blocking_vnet_name = None
        
        if not use_layout_aware_rules:
            # Only check for blocking VNets for different-hub connections
            logging.debug(f"  Checking for blocking VNets between {source_name} and {target_name} (different-hub connection)")
            logging.debug(f"  Source bounds: x=[{source_x}, {source_x + VNET_WIDTH}], y=[{source_y}, {source_y + source_height}]")
            logging.debug(f"  Target bounds: x=[{target_x}, {target_x + VNET_WIDTH}], y=[{target_y}, {target_y + target_height}]")
            
            # Calculate bounding box between source and target
            min_x = min(source_x, target_x)
            max_x = max(source_x + VNET_WIDTH, target_x + VNET_WIDTH)
            min_y = min(source_y, target_y)
            max_y = max(source_y + source_height, target_y + target_height)
            logging.debug(f"  Bounding box: x=[{min_x}, {max_x}], y=[{min_y}, {max_y}]")
            
            for vnet_name, (v_x, v_y, v_height) in all_vnet_positions.items():
                # Skip source and target VNets
                if vnet_name == source_name or vnet_name == target_name:
                    continue
                
                vnet_left_edge = v_x
                vnet_right_edge = v_x + VNET_WIDTH
                vnet_top_y = v_y
                vnet_bottom_y = v_y + v_height
                
                # Check if VNet overlaps the bounding box between source and target
                vnet_overlaps_horizontally = not (vnet_right_edge <= min_x or vnet_left_edge >= max_x)
                vnet_overlaps_vertically = not (vnet_bottom_y <= min_y or vnet_top_y >= max_y)
                
                logging.debug(f"  Checking {vnet_name}: x=[{v_x}, {v_x + VNET_WIDTH}], y=[{v_y}, {v_y + v_height}]")
                logging.debug(f"    Overlaps horizontally: {vnet_overlaps_horizontally}, vertically: {vnet_overlaps_vertically}")
                
                if vnet_overlaps_horizontally and vnet_overlaps_vertically:
                    vnet_in_path = True
                    blocking_vnet_x = v_x
                    blocking_vnet_y = v_y
                    blocking_vnet_height = v_height
                    blocking_vnet_name = vnet_name
                    logging.debug(f"  *** BLOCKING VNet FOUND: {vnet_name} ***")
                    break
            
            if not vnet_in_path:
                logging.debug(f"  No blocking VNet found - direct connection")
        else:
            # Layout-aware connections (hubless and same-hub) handle blocking detection internally
            logging.debug(f"  Skipping blocking detection for layout-aware connection (handled by create_hubless_connection)")
        
        # Calculate actual connection points (side centers)
        if target_x < source_x:
            # Target is to the left: source connects from left side, target connects to right side
            source_connection_x = source_x  # Left edge
            target_connection_x = target_x + VNET_WIDTH  # Right edge
        else:
            # Target is to the right: source connects from right side, target connects to left side
            source_connection_x = source_x + VNET_WIDTH  # Right edge
            target_connection_x = target_x  # Left edge
        
        source_connection_y = source_center_y  # Side center Y
        target_connection_y = target_center_y  # Side center Y
        
        if vnet_in_path and blocking_vnet_x is not None and blocking_vnet_y is not None and blocking_vnet_height is not None:
            logging.debug(f"  Routing around blocking VNet: {blocking_vnet_name}")
            # Route around blocking VNet: determine best path (left or right, above or below)
            source_left_edge = source_x
            source_right_edge = source_x + VNET_WIDTH
            blocking_left_edge = blocking_vnet_x
            blocking_right_edge = blocking_vnet_x + VNET_WIDTH
            target_left_edge = target_x
            target_right_edge = target_x + VNET_WIDTH
            
            # Calculate space on left and right sides
            space_left = min(abs(source_left_edge - blocking_right_edge), abs(target_left_edge - blocking_right_edge)) if source_x < blocking_vnet_x or target_x < blocking_vnet_x else min(abs(blocking_left_edge - source_right_edge), abs(blocking_left_edge - target_right_edge))
            space_right = min(abs(source_right_edge - blocking_left_edge), abs(target_right_edge - blocking_left_edge)) if source_x > blocking_vnet_x or target_x > blocking_vnet_x else min(abs(blocking_right_edge - source_left_edge), abs(blocking_right_edge - target_left_edge))
            
            # Determine if we should route above or below blocking VNet
            route_above = source_connection_y < blocking_vnet_y and target_connection_y < blocking_vnet_y
            route_below = source_connection_y > (blocking_vnet_y + blocking_vnet_height) and target_connection_y > (blocking_vnet_y + blocking_vnet_height)
            
            # Default: route around the side with more space
            route_h_offset = _config.routing_waypoint_offset_horizontal
            route_v_offset = _config.routing_waypoint_offset_vertical
            
            # Add waypoints
            array_elem = SubElement(mxGeometry, "Array")
            array_elem.set("as", "points")
            
            if space_left > space_right:
                # Route left: go left from source side, then up/down around blocking VNet, then right to target side
                waypoint1_x = source_connection_x - route_h_offset  # Go left from source side
                waypoint1_y = source_connection_y  # Same Y as source side center
                
                if route_above:
                    waypoint2_y = blocking_vnet_y - route_v_offset  # Above blocking VNet
                elif route_below:
                    waypoint2_y = blocking_vnet_y + blocking_vnet_height + route_v_offset  # Below blocking VNet
                else:
                    # Route through middle: choose the side with more vertical space
                    space_above = abs(source_connection_y - blocking_vnet_y) if source_connection_y < blocking_vnet_y else abs(target_connection_y - blocking_vnet_y)
                    space_below = abs(source_connection_y - (blocking_vnet_y + blocking_vnet_height)) if source_connection_y > (blocking_vnet_y + blocking_vnet_height) else abs(target_connection_y - (blocking_vnet_y + blocking_vnet_height))
                    waypoint2_y = blocking_vnet_y - route_v_offset if space_above > space_below else blocking_vnet_y + blocking_vnet_height + route_v_offset
                
                waypoint2_x = min(source_x, target_x) - route_h_offset  # Stay left
                waypoint3_x = waypoint2_x  # Stay left
                waypoint3_y = waypoint2_y  # Stay at blocking VNet level
                waypoint4_x = target_connection_x - route_h_offset  # Approach target from left
                waypoint4_y = target_connection_y  # Same Y as target side center
            else:
                # Route right: go right from source side, then up/down around blocking VNet, then left to target side
                waypoint1_x = source_connection_x + route_h_offset  # Go right from source side
                waypoint1_y = source_connection_y  # Same Y as source side center
                
                if route_above:
                    waypoint2_y = blocking_vnet_y - route_v_offset  # Above blocking VNet
                elif route_below:
                    waypoint2_y = blocking_vnet_y + blocking_vnet_height + route_v_offset  # Below blocking VNet
                else:
                    # Route through middle: choose the side with more vertical space
                    space_above = abs(source_connection_y - blocking_vnet_y) if source_connection_y < blocking_vnet_y else abs(target_connection_y - blocking_vnet_y)
                    space_below = abs(source_connection_y - (blocking_vnet_y + blocking_vnet_height)) if source_connection_y > (blocking_vnet_y + blocking_vnet_height) else abs(target_connection_y - (blocking_vnet_y + blocking_vnet_height))
                    waypoint2_y = blocking_vnet_y - route_v_offset if space_above > space_below else blocking_vnet_y + blocking_vnet_height + route_v_offset
                
                waypoint2_x = max(source_x, target_x) + VNET_WIDTH + route_h_offset  # Stay right
                waypoint3_x = waypoint2_x  # Stay right
                waypoint3_y = waypoint2_y  # Stay at blocking VNet level
                waypoint4_x = target_connection_x + route_h_offset  # Approach target from right
                waypoint4_y = target_connection_y  # Same Y as target side center
            
            waypoint1 = SubElement(array_elem, "mxPoint")
            waypoint1.set("x", str(waypoint1_x))
            waypoint1.set("y", str(waypoint1_y))
            
            waypoint2 = SubElement(array_elem, "mxPoint")
            waypoint2.set("x", str(waypoint2_x))
            waypoint2.set("y", str(waypoint2_y))
            
            waypoint3 = SubElement(array_elem, "mxPoint")
            waypoint3.set("x", str(waypoint3_x))
            waypoint3.set("y", str(waypoint3_y))
            
            waypoint4 = SubElement(array_elem, "mxPoint")
            waypoint4.set("x", str(waypoint4_x))
            waypoint4.set("y", str(waypoint4_y))
        else:
            # No blocking VNet: direct connection with waypoints to ensure clean routing
            route_h_offset = _config.routing_waypoint_offset_horizontal
            
            array_elem = SubElement(mxGeometry, "Array")
            array_elem.set("as", "points")
            
            # Waypoint 1: Just outside source side
            waypoint1_x = source_connection_x + (route_h_offset if target_x > source_x else -route_h_offset)
            waypoint1_y = source_connection_y
            
            # Waypoint 2: Just outside target side (at same Y level if horizontal, or adjust if vertical)
            waypoint2_x = target_connection_x + (-route_h_offset if target_x > source_x else route_h_offset)
            waypoint2_y = target_connection_y
            
            waypoint1 = SubElement(array_elem, "mxPoint")
            waypoint1.set("x", str(waypoint1_x))
            waypoint1.set("y", str(waypoint1_y))
            
            waypoint2 = SubElement(array_elem, "mxPoint")
            waypoint2.set("x", str(waypoint2_x))
            waypoint2.set("y", str(waypoint2_y))
    elif source_is_hub and target_is_hub and source_x is not None and target_x is not None and source_y is not None and target_y is not None and source_height is not None and target_height is not None:
        # Hub-to-Hub: Use dedicated module
        from gettopology.diagram.connections.hub_to_hub import create_hub_to_hub_connection
        create_hub_to_hub_connection(
            root, mxGeometry,
            source_x, target_x,
            source_y, target_y,
            source_height, target_height
        )
    # Check if source is a hubless center
    is_source_hubless_center = False
    if hubless_centers:
        is_source_hubless_center = source_name in hubless_centers.values()
    
    # Check if target is a hubless center (for Spoke → Central Spoke - should not use Hub → Spoke pattern)
    is_target_hubless_center = False
    if hubless_centers:
        is_target_hubless_center = target_name in hubless_centers.values()
    
    # Hub → Spoke OR Central Spoke → Spoke: Hub/center connects from bottom center to spoke side middle
    if (source_is_hub or is_source_hubless_center) and not target_is_hub and not is_target_hubless_center and source_x is not None and target_x is not None and source_y is not None and target_y is not None and source_height is not None:
        # Regular hub or hubless spoke center: Use dedicated module
        create_hub_to_spoke_connection(
            root, mxGeometry,
            source_x, target_x,
            source_y, target_y,
            source_height, target_height
        )
    
    return cell_id


def generate_hld_diagram(topology: TopologyModel, output_path: str) -> None:
    """Generate High-Level Diagram (HLD) in Draw.io XML format.
    
    HLD shows:
    - VNet name
    - Address space
    - Subscription name / Resource group
    - Peering connections between VNets
    
    Layout:
    - Hubs at the top (or center)
    - Hub spokes below hubs
    - Hubless spokes below hub spokes
    - Stubs at the bottom
    
    Args:
        topology: TopologyModel containing all virtual networks
        output_path: Path to save the Draw.io XML file
    """
    if not topology.virtual_networks:
        logging.warning("No virtual networks to diagram")
        return
    
    # Categorize VNets
    categories = categorize_vnets(topology)
    
    # Group hubs with their spokes
    hub_spoke_groups, hubless_groups, stubs = group_hubs_with_spokes(categories)
    
    # Create Draw.io XML structure
    mxfile = Element("mxfile")
    mxfile.set("host", "app.diagrams.net")
    mxfile.set("modified", "2024-01-01T00:00:00.000Z")
    mxfile.set("agent", "gettopology")
    mxfile.set("version", "22.1.0")
    mxfile.set("etag", "dummy")
    mxfile.set("type", "device")
    
    diagram = SubElement(mxfile, "diagram")
    diagram.set("id", "topology-hld")
    diagram.set("name", "Azure VNet Topology - HLD")
    
    mxGraphModel = SubElement(diagram, "mxGraphModel")
    mxGraphModel.set("dx", str(_config.drawio_page_dx))
    mxGraphModel.set("dy", str(_config.drawio_page_dy))
    mxGraphModel.set("grid", "1")
    mxGraphModel.set("gridSize", str(_config.drawio_grid_size))
    mxGraphModel.set("guides", "1")
    mxGraphModel.set("tooltips", "1")
    mxGraphModel.set("connect", "1")
    mxGraphModel.set("arrows", "1")
    mxGraphModel.set("fold", "1")
    mxGraphModel.set("page", "1")
    mxGraphModel.set("pageWidth", str(_config.page_width))
    mxGraphModel.set("pageHeight", str(_config.page_height))
    mxGraphModel.set("math", "0")
    mxGraphModel.set("shadow", "0")
    
    root = SubElement(mxGraphModel, "root")
    
    # Add mxCell for root
    root_cell = SubElement(root, "mxCell")
    root_cell.set("id", "0")
    
    root_cell_layer = SubElement(root, "mxCell")
    root_cell_layer.set("id", "1")
    root_cell_layer.set("parent", "0")
    
    # Legend will be created after calculating all positions to avoid overlaps
    cell_id_counter = _config.cell_id_start
    legend_x = _config.legend_x
    legend_y = _config.legend_y  # Will be adjusted after position calculation
    
    # Create mapping of VNet names to main cell IDs (for edge connections)
    vnet_name_to_main_id: dict[str, int] = {}
    # Create mapping of VNet names to tenant IDs (for cross-tenant detection)
    vnet_name_to_tenant_id: dict[str, str] = {vnet.name: vnet.tenant_id for vnet in topology.virtual_networks}
    # Track which VNets are hubs (for edge styling)
    hub_names: set[str] = {vnet.name for vnet in categories.hubs}
    # Track which VNets are hubless spokes (for edge styling - to distinguish from hub-spoke)
    # Flatten all hubless groups into a single set
    hubless_spoke_names: set[str] = {vnet.name for group in hubless_groups for vnet in group}
    # Track the center VNet for each hubless group (acts as hub for connection purposes)
    # Map: group_index -> center_name
    hubless_centers: dict[int, str] = {}
    # Track which hubless group each VNet belongs to: vnet_name -> group_index
    hubless_group_map: dict[str, int] = {}
    for group_index, group in enumerate(hubless_groups):
        # Find center (most connected VNet in this group)
        center_vnet = max(group, key=lambda v: v.peering_count)
        hubless_centers[group_index] = center_vnet.name
        # Map all VNets in this group to the group index
        for vnet in group:
            hubless_group_map[vnet.name] = group_index
    # Track hub positions for hub-to-hub connections
    hub_name_to_position: dict[str, tuple[float, float, float]] = {}  # (x, y, height)
    # Track hubless spoke positions for external VNet connections
    hubless_spoke_positions: dict[str, tuple[float, float, float]] = {}  # (x, y, height)
    # Track regular spoke positions for external VNet connections
    regular_spoke_positions: dict[str, tuple[float, float, float]] = {}  # (x, y, height)
    # Track all VNet positions (hubs + regular spokes + hubless spokes) for overlap detection
    all_vnet_positions: dict[str, tuple[float, float, float]] = {}  # (x, y, height)
    # cell_id_counter already initialized above for legend creation
    
    # Store edge information to create after all VNets (for proper z-ordering)
    # Format: (source_id, target_id, is_local, source_is_hub, target_is_hub, source_x, target_x, source_y, target_y, source_height, target_height, source_name, target_name, is_cross_tenant)
    edges_to_create: list[tuple[int, int, bool, bool, bool, float | None, float | None, float | None, float | None, float | None, float | None, str, str, bool]] = []
    created_edges: set[tuple[int, int]] = set()  # Track to avoid duplicates
    
    def _add_edge_if_not_exists(
        source_id: int, 
        target_id: int, 
        is_local: bool,
        source_name: str,
        target_name: str,
        source_x: float | None = None,
        target_x: float | None = None,
        source_y: float | None = None,
        target_y: float | None = None,
        source_height: float | None = None,
        target_height: float | None = None,
    ) -> None:
        """Helper to add edge to list only if it doesn't already exist (bidirectional check)."""
        edge_key = tuple(sorted([source_id, target_id]))
        if edge_key not in created_edges:
            # For hubless spokes, the center VNet should be treated as a hub for connection purposes
            # Check if source is a hubless center (acts as hub for connections)
            is_hubless_center = False
            if hubless_centers:
                is_hubless_center = source_name in hubless_centers.values()
            source_is_hub = (source_name in hub_names and source_name not in hubless_spoke_names) or is_hubless_center
            target_is_hub = target_name in hub_names and target_name not in hubless_spoke_names
            # Check if this is cross-tenant: different tenant_id or external VNet (not in vnet_name_to_tenant_id)
            source_tenant_id = vnet_name_to_tenant_id.get(source_name)
            target_tenant_id = vnet_name_to_tenant_id.get(target_name)
            is_cross_tenant = (source_tenant_id is not None and target_tenant_id is not None and source_tenant_id != target_tenant_id) or (target_name not in vnet_name_to_tenant_id)  # External VNet is cross-tenant
            edges_to_create.append((source_id, target_id, is_local, source_is_hub, target_is_hub, source_x, target_x, source_y, target_y, source_height, target_height, source_name, target_name, is_cross_tenant))
            created_edges.add(edge_key)
    
    # Determine primary subscription ID (first hub's subscription)
    primary_subscription_id = hub_spoke_groups[0].hub.subscription_id if hub_spoke_groups else None
    
    # Calculate zone positions using layout module (separates hub-spoke from hubless)
    zones, hubless_zones = calculate_zone_positions(hub_spoke_groups, hubless_groups)
    
    # Create main page
    from gettopology.diagram.pages.main_page import create_main_page
    (
        cell_id_counter,
        vnet_name_to_main_id,
        hub_name_to_position,
        regular_spoke_positions,
        hubless_spoke_positions,
        all_vnet_positions,
        edges_to_create,
    ) = create_main_page(
        root, topology, zones, primary_subscription_id, hubless_spoke_names, stubs,
        hub_names, vnet_name_to_tenant_id, hubless_group_map, hubless_centers,
        cell_id_counter, legend_x, _create_peering_edge
    )
    
    # Create all edges from edges_to_create list
    for edge_data in edges_to_create:
        if len(edge_data) == 14:
            source_id, target_id, is_local, source_is_hub, target_is_hub, source_x, target_x, source_y, target_y, source_height, target_height, source_name, target_name, is_cross_tenant = edge_data
        elif len(edge_data) == 13:
            # Backward compatibility: old format without is_cross_tenant
            source_id, target_id, is_local, source_is_hub, target_is_hub, source_x, target_x, source_y, target_y, source_height, target_height, source_name, target_name = edge_data
            # Check tenant_id if available
            source_tenant_id = vnet_name_to_tenant_id.get(source_name)
            target_tenant_id = vnet_name_to_tenant_id.get(target_name)
            is_cross_tenant = (source_tenant_id is not None and target_tenant_id is not None and source_tenant_id != target_tenant_id) or (target_name not in vnet_name_to_tenant_id)
        else:
            # Backward compatibility: old format without names
            source_id, target_id, is_local, source_is_hub, target_is_hub, source_x, target_x, source_y, target_y, source_height, target_height = edge_data
            source_name = ""
            target_name = ""
            is_cross_tenant = False
        _create_peering_edge(
            root, cell_id_counter, source_id, target_id, is_local,
            source_is_hub, target_is_hub, source_x, target_x, source_y, target_y,
            source_height, target_height, source_name, target_name,
            hubless_spoke_names, None, hubless_group_map, hubless_centers,
            is_cross_tenant, hub_name_to_position, all_vnet_positions
        )
        cell_id_counter += 1
    
    # Create second page for hubless spokes if they exist
    from gettopology.diagram.pages.hubless_page import create_hubless_page
    create_hubless_page(
        mxfile, hubless_groups, primary_subscription_id, hubless_spoke_names,
        _create_peering_edge, _config.cell_id_start
    )
    
    # Create third page for orphan/stub VNets if they exist
    from gettopology.diagram.pages.orphan_page import create_orphan_page
    create_orphan_page(mxfile, stubs, _config.cell_id_start)
    
    # Write to file
    xml_str = minidom.parseString(tostring(mxfile)).toprettyxml(indent="  ")
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(xml_str)
    
    logging.info(f"HLD diagram saved to {output_path}")


def generate_lld_diagram(topology: TopologyModel, output_path: str) -> None:
    """Generate Low-Level Diagram (LLD) in Draw.io XML format.
    
    LLD shows all HLD details plus:
    - Subnets
    - Subnet address ranges
    - Private endpoints
    - NSG associations
    - Route table associations
    - Subnet delegations
    
    Args:
        topology: TopologyModel containing all virtual networks
        output_path: Path to save the Draw.io XML file
    """
    # TODO: Implement LLD generation
    logging.info("LLD diagram generation not yet implemented")
    pass
