# slice_deployment.py
"""
Core slice deployment functionality.
Handles FABRIC slice creation and resource provisioning.
"""

import logging
from typing import Optional, List
from datetime import datetime

from fabrictestbed_extensions.fablib.fablib import FablibManager as fablib
from .models import SiteTopology, Node, NIC, DPU, FPGA, GPU, NVMe, PersistentVolume, FacilityPort

logger = logging.getLogger(__name__)


class SliceDeploymentError(Exception):
    """Raised when slice deployment fails."""
    pass


def check_or_generate_unique_slice_name(base_name: str, use_timestamp: bool = False) -> str:
    """
    Ensure the slice name is unique by checking existing slices.
    
    Args:
        base_name: Proposed base name for the slice
        use_timestamp: Whether to append a timestamp for uniqueness
        
    Returns:
        A unique slice name
    """
    fab = fablib()
    try:
        existing_names = [slice.get_name() for slice in fab.get_slices()]
        
        if base_name not in existing_names:
            return base_name
        
        if use_timestamp:
            timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
            return f"{base_name}-{timestamp}"
        
        # Use numeric suffix
        i = 1
        new_name = f"{base_name}-{i}"
        while new_name in existing_names:
            i += 1
            new_name = f"{base_name}-{i}"
        return new_name
        
    except Exception as e:
        logger.error(f"Error checking/generating slice name: {e}")
        return base_name


def add_gpus_to_node(fab_node, node: Node) -> None:
    """
    Add GPU components to a FABRIC node.
    
    Args:
        fab_node: FABRIC node object
        node: Node model with GPU configuration
    """
    for gpu_id, gpu in node.pci.gpu.items():
        try:
            fab_node.add_component(name=gpu.name, model=gpu.model)
            logger.debug(f"Added GPU {gpu.name} ({gpu.model}) to {node.hostname}")
        except Exception as e:
            logger.warning(f"Failed to add GPU {gpu.name} to {node.hostname}: {e}")


def add_dpus_to_node(fab_node, node: Node) -> None:
    """
    Add DPU components to a FABRIC node.
    
    Args:
        fab_node: FABRIC node object
        node: Node model with DPU configuration
    """
    for dpu_id, dpu in node.pci.dpu.items():
        try:
            fab_node.add_component(name=dpu.name, model=dpu.model)
            logger.debug(f"Added DPU {dpu.name} ({dpu.model}) to {node.hostname}")
        except Exception as e:
            logger.warning(f"Failed to add DPU {dpu.name} to {node.hostname}: {e}")


def add_fpgas_to_node(fab_node, node: Node) -> None:
    """
    Add FPGA components to a FABRIC node.
    
    Args:
        fab_node: FABRIC node object
        node: Node model with FPGA configuration
    """
    for fpga_id, fpga in node.pci.fpga.items():
        try:
            fab_node.add_component(name=fpga.name, model=fpga.model)
            logger.debug(f"Added FPGA {fpga.name} ({fpga.model}) to {node.hostname}")
        except Exception as e:
            logger.warning(f"Failed to add FPGA {fpga.name} to {node.hostname}: {e}")


def add_nvmes_to_node(fab_node, node: Node) -> None:
    """
    Add NVMe components to a FABRIC node.
    
    Args:
        fab_node: FABRIC node object
        node: Node model with NVMe configuration
    """
    for nvme_id, nvme in node.pci.nvme.items():
        try:
            fab_node.add_component(name=nvme.name, model=nvme.model)
            logger.debug(f"Added NVMe {nvme.name} ({nvme.model}) to {node.hostname}")
        except Exception as e:
            logger.warning(f"Failed to add NVMe {nvme.name} to {node.hostname}: {e}")


def add_nics_to_node(fab_node, node: Node) -> None:
    """
    Add NIC components to a FABRIC node.
    
    Args:
        fab_node: FABRIC node object
        node: Node model with NIC configuration
    """
    for nic_id, nic in node.pci.network.items():
        try:
            fab_node.add_component(name=nic.name, model=nic.model)
            logger.debug(f"Added NIC {nic.name} ({nic.model}) to {node.hostname}")
        except Exception as e:
            logger.warning(f"Failed to add NIC {nic.name} to {node.hostname}: {e}")


def add_persistent_storage_to_node(fab_node, node: Node) -> None:
    """
    Add persistent storage volumes to a FABRIC node.
    
    Args:
        fab_node: FABRIC node object
        node: Node model with storage configuration
    """
    for vol_id, volume in node.persistent_storage.volume.items():
        try:
            fab_node.add_storage(name=volume.name)
            logger.debug(f"Added storage {volume.name} ({volume.size}GB) to {node.hostname}")
        except Exception as e:
            logger.warning(f"Failed to add storage {volume.name} to {node.hostname}: {e}")


def add_postboot_commands_to_node(fab_node, node: Node) -> None:
    """
    Add post-boot commands to a FABRIC node if specified in topology.
    
    Args:
        fab_node: FABRIC node object
        node: Node model with potential postboot commands
    """
    if node.specific.has_postboot_commands():
        try:
            postboot_cmd = node.specific.postboot.strip()
            fab_node.add_post_boot_execute(command=postboot_cmd)
            logger.info(f"Added post-boot command to {node.hostname}")
            logger.debug(f"Post-boot command: {postboot_cmd}")
            print(f"   🔧 Added post-boot commands to {node.hostname}")
        except Exception as e:
            logger.warning(f"Failed to add post-boot command to {node.hostname}: {e}")
            print(f"   ⚠️ Failed to add post-boot command to {node.hostname}: {e}")


def add_facility_ports_to_slice(topology: SiteTopology, slice) -> dict:
    """
    Add facility ports to the slice.
    
    Args:
        topology: Site topology model
        slice: FABRIC slice object
        
    Returns:
        Dictionary mapping facility port names to FABRIC facility port objects
    """
    facility_port_objects = {}
    
    if not topology.has_facility_ports():
        logger.debug("No facility ports defined in topology")
        return facility_port_objects
    
    logger.info("Adding facility ports to slice")
    print("\n🔌 Adding facility ports...\n")
    
    for fp_id, fp in topology.site_topology_facility_ports.facility_ports.items():
        try:
            logger.info(f"Adding facility port: {fp.name} at {fp.site} (VLAN {fp.vlan})")
            print(f"   🔌 {fp.name} @ {fp.site} - VLAN {fp.vlan} → {fp.binding}")
            
            fab_fp = slice.add_facility_port(
                name=fp.name,
                site=fp.site,
                vlan=str(fp.vlan)
            )
            
            facility_port_objects[fp.name] = fab_fp
            logger.info(f"Successfully added facility port: {fp.name}")
            
        except Exception as e:
            logger.error(f"Failed to add facility port {fp.name}: {e}")
            print(f"   ❌ Failed to add facility port {fp.name}: {e}")
            continue
    
    return facility_port_objects


def create_and_bind_networks(topology: SiteTopology, slice, facility_port_objects: dict = None) -> dict:
    """
    Create FABRIC networks and bind node interfaces and facility ports to them.
    Handles NIC, DPU, FPGA interfaces, and facility ports.
    
    Args:
        topology: Site topology model
        slice: FABRIC slice object
        facility_port_objects: Dictionary of facility port objects (optional)
        
    Returns:
        Dictionary mapping network names to FABRIC network objects
    """
    if facility_port_objects is None:
        facility_port_objects = {}
    
    network_objects = {}
    
    # Create networks
    for net_id, network in topology.site_topology_networks.networks.items():
        try:
            # Layer 2 networks
            if network.type in ["L2Bridge", "L2PTP", "L2STS"]:
                net = slice.add_l2network(name=network.name, type=network.type)
                logger.info(f"Created L2 network: {network.name} ({network.type})")
            
            # Layer 3 networks (orchestrator-managed)
            elif network.type in ["IPv4", "IPv6", "IPv4Ext", "IPv6Ext"]:
                net = slice.add_l3network(name=network.name, type=network.type)
                logger.info(f"Created L3 network: {network.name} ({network.type})")
            
            else:
                logger.warning(f"Unsupported network type: {network.type}")
                continue
            
            network_objects[network.name] = net
            
        except Exception as e:
            logger.error(f"Failed to create network {network.name}: {e}")
            continue
    
    # Bind facility port interfaces to networks
    if topology.has_facility_ports():
        logger.info("Binding facility port interfaces to networks")
        
        for fp_id, fp in topology.site_topology_facility_ports.facility_ports.items():
            if fp.name not in facility_port_objects:
                logger.warning(f"Facility port {fp.name} not found in facility_port_objects")
                continue
            
            if fp.binding not in network_objects:
                logger.warning(f"Network '{fp.binding}' not found for facility port {fp.name}")
                continue
            
            try:
                fab_fp = facility_port_objects[fp.name]
                fp_interfaces = fab_fp.get_interfaces()
                
                if fp_interfaces:
                    network_objects[fp.binding].add_interface(fp_interfaces[0])
                    logger.info(f"Connected facility port {fp.name} to network {fp.binding}")
                    print(f"   ✅ Connected facility port {fp.name} to {fp.binding}")
                else:
                    logger.warning(f"No interfaces found for facility port {fp.name}")
                    print(f"   ⚠️ No interfaces found for facility port {fp.name}")
                    
            except Exception as e:
                logger.error(f"Failed to bind facility port {fp.name} to network {fp.binding}: {e}")
                print(f"   ❌ Failed to bind facility port {fp.name}: {e}")
    
    # Bind node NIC interfaces to networks
    for node_id, node in topology.site_topology_nodes.nodes.items():
        try:
            fab_node = slice.get_node(name=node.hostname)
        except Exception as e:
            logger.error(f"Could not retrieve node '{node.hostname}': {e}")
            continue
        
        # Process NICs
        for nic_id, nic in node.pci.network.items():
            try:
                fab_nic = fab_node.get_component(name=nic.name)
                iface_list = fab_nic.get_interfaces()
            except Exception as e:
                logger.error(f"Could not retrieve NIC '{nic.name}' on node '{node.hostname}': {e}")
                continue
            
            for i, (iface_name, iface) in enumerate(nic.interfaces.items()):
                if not iface.binding:
                    continue
                
                if iface.binding not in network_objects:
                    logger.warning(f"Network '{iface.binding}' not found in topology")
                    continue
                
                if i < len(iface_list):
                    network_objects[iface.binding].add_interface(iface_list[i])
                    logger.info(f"Connected {node.hostname}.{nic.name}.{iface_name} to {iface.binding}")
                else:
                    logger.warning(f"Interface index out of range: {node.hostname}.{nic.name}.{iface_name}")
        
        # Process DPUs (same logic as NICs)
        for dpu_id, dpu in node.pci.dpu.items():
            try:
                fab_dpu = fab_node.get_component(name=dpu.name)
                iface_list = fab_dpu.get_interfaces()
            except Exception as e:
                logger.error(f"Could not retrieve DPU '{dpu.name}' on node '{node.hostname}': {e}")
                continue
            
            for i, (iface_name, iface) in enumerate(dpu.interfaces.items()):
                if not iface.binding:
                    continue
                
                if iface.binding not in network_objects:
                    logger.warning(f"Network '{iface.binding}' not found in topology")
                    continue
                
                if i < len(iface_list):
                    network_objects[iface.binding].add_interface(iface_list[i])
                    logger.info(f"Connected {node.hostname}.{dpu.name}.{iface_name} to {iface.binding}")
                else:
                    logger.warning(f"Interface index out of range: {node.hostname}.{dpu.name}.{iface_name}")
        
        # Process FPGAs (same logic as NICs and DPUs)
        for fpga_id, fpga in node.pci.fpga.items():
            try:
                fab_fpga = fab_node.get_component(name=fpga.name)
                iface_list = fab_fpga.get_interfaces()
            except Exception as e:
                logger.error(f"Could not retrieve FPGA '{fpga.name}' on node '{node.hostname}': {e}")
                continue
            
            for i, (iface_name, iface) in enumerate(fpga.interfaces.items()):
                if not iface.binding:
                    continue
                
                if iface.binding not in network_objects:
                    logger.warning(f"Network '{iface.binding}' not found in topology")
                    continue
                
                if i < len(iface_list):
                    network_objects[iface.binding].add_interface(iface_list[i])
                    logger.info(f"Connected {node.hostname}.{fpga.name}.{iface_name} to {iface.binding}")
                else:
                    logger.warning(f"Interface index out of range: {node.hostname}.{fpga.name}.{iface_name}")
    
    return network_objects


def configure_l3_networks(slice, topology: SiteTopology) -> None:
    """
    Configure L3 (IPv4/IPv6) networks after slice submission.
    
    This function:
    1. Gets available IP addresses from the orchestrator-assigned subnet
    2. Assigns IPs to node interfaces (both NIC and DPU interfaces)
    3. Configures routing between L3 networks for each node
    
    NOTE: For IPv4Ext/IPv6Ext networks, call enable_public_routing() separately
    to enable public internet access.
    
    Args:
        slice: FABRIC slice object (must be already submitted)
        topology: Site topology model
        
    Raises:
        SliceDeploymentError: If L3 network configuration fails
    """
    logger.info("Starting L3 network configuration")
    print("\n🌐 Configuring L3 networks (IPv4/IPv6)...\n")
    
    try:
        fab = fablib()
        
        # Store network information for routing configuration
        network_info = {}  # {network_name: {'subnet': subnet, 'gateway': gateway, 'type': type}}
        node_network_mapping = {}  # {node_hostname: [list of connected network names]}
        
        # Process each network
        for network_model in topology.site_topology_networks.iter_networks():
            # Only process L3 networks (IPv4, IPv4Ext, IPv6, IPv6Ext)
            if network_model.type not in ["IPv4", "IPv4Ext", "IPv6", "IPv6Ext"]:
                continue
            
            network_name = network_model.name
            logger.info(f"Configuring L3 network: {network_name} (type: {network_model.type})")
            print(f"🔧 Configuring network: {network_name} ({network_model.type})")
            
            try:
                # Get the network from the slice
                fabric_network = slice.get_network(name=network_name)
                
                # Get available IPs from orchestrator
                available_ips = fabric_network.get_available_ips()
                logger.info(f"Network {network_name} has {len(available_ips)} available IPs")
                print(f"   📋 Available IPs: {len(available_ips)}")
                
                if not available_ips:
                    logger.warning(f"No available IPs for network {network_name}")
                    print(f"   ⚠️ No available IPs for {network_name}")
                    continue
                
                # Get the subnet for this network
                network_subnet = fabric_network.get_subnet()
                network_gateway = fabric_network.get_gateway()
                logger.debug(f"Network {network_name} subnet: {network_subnet}, gateway: {network_gateway}")
                
                # Store network info for routing configuration
                network_info[network_name] = {
                    'subnet': network_subnet,
                    'gateway': network_gateway,
                    'type': network_model.type
                }
                
                # Get all nodes connected to this network
                connected_nodes = topology.get_nodes_on_network(network_name)
                
                # Configure each node's interface
                for node_model in connected_nodes:
                    try:
                        fab_node = slice.get_node(name=node_model.hostname)
                        fab_iface = fab_node.get_interface(network_name=network_name)
                        
                        # Track which networks each node is connected to
                        if node_model.hostname not in node_network_mapping:
                            node_network_mapping[node_model.hostname] = []
                        node_network_mapping[node_model.hostname].append(network_name)
                        
                        # Pop the first available IP
                        if not available_ips:
                            logger.error(f"Ran out of IPs for network {network_name}")
                            print(f"   ❌ No more IPs available for {node_model.hostname}")
                            break
                        
                        node_ip = available_ips.pop(0)
                        
                        # Assign IP to interface
                        fab_iface.ip_addr_add(addr=node_ip, subnet=network_subnet)
                        logger.info(f"Assigned {node_ip} to {node_model.hostname}")
                        print(f"   ✅ {node_model.hostname}: {node_ip}")
                        
                    except Exception as e:
                        logger.error(f"Failed to configure {node_model.hostname} on {network_name}: {e}")
                        print(f"   ❌ Error configuring {node_model.hostname}: {e}")
                        continue
                
            except Exception as e:
                logger.error(f"Failed to configure network {network_name}: {e}")
                print(f"   ❌ Error configuring network: {e}")
                continue
        
        # Configure inter-network routing for each node
        logger.info("Starting inter-network routing configuration")
        print("\n🔀 Configuring inter-network routing...\n")
        
        for node_hostname, connected_networks in node_network_mapping.items():
            try:
                fab_node = slice.get_node(name=node_hostname)
                
                # Get all L3 networks in topology
                all_l3_networks = [net_name for net_name in network_info.keys()]
                
                # Find networks this node is NOT connected to
                other_networks = [net for net in all_l3_networks if net not in connected_networks]
                
                if not other_networks:
                    logger.debug(f"Node {node_hostname} is connected to all L3 networks, no additional routes needed")
                    print(f"ℹ️  {node_hostname}: Connected to all networks, no routes needed")
                    continue
                
                logger.info(f"Configuring routes on {node_hostname}")
                print(f"🔧 Configuring routes on {node_hostname}:")
                
                routes_added = 0
                
                # For each network this node is NOT connected to, add routes via connected networks
                for target_network in other_networks:
                    target_subnet = network_info[target_network]['subnet']
                    target_type = network_info[target_network]['type']
                    
                    # Determine which gateway to use based on IP version compatibility
                    # IPv4 routes use IPv4 gateways, IPv6 routes use IPv6 gateways
                    is_ipv6_target = target_type in ["IPv6", "IPv6Ext"]
                    
                    # Find a suitable gateway from the networks this node IS connected to
                    gateway_found = False
                    for connected_network in connected_networks:
                        connected_type = network_info[connected_network]['type']
                        is_ipv6_connected = connected_type in ["IPv6", "IPv6Ext"]
                        
                        # Match IPv4 with IPv4, IPv6 with IPv6
                        if is_ipv6_target == is_ipv6_connected:
                            gateway = network_info[connected_network]['gateway']
                            
                            try:
                                fab_node.ip_route_add(subnet=target_subnet, gateway=gateway)
                                logger.info(f"  Added route on {node_hostname}: {target_subnet} via {gateway} ({connected_network})")
                                print(f"   ✅ Route: {target_network} ({target_subnet}) via {connected_network} ({gateway})")
                                routes_added += 1
                                gateway_found = True
                                break  # Only add one route per target network
                            except Exception as e:
                                logger.warning(f"  Failed to add route on {node_hostname} for {target_subnet} via {gateway}: {e}")
                                print(f"   ⚠️  Route failed: {target_network} via {gateway}: {e}")
                    
                    if not gateway_found:
                        logger.warning(f"  No suitable gateway found on {node_hostname} for {target_network} ({target_type})")
                        print(f"   ⚠️  No suitable gateway for {target_network} ({target_type})")
                
                if routes_added > 0:
                    logger.info(f"Added {routes_added} routes on {node_hostname}")
                    print(f"   📊 Total routes added: {routes_added}")
                else:
                    logger.info(f"No routes added on {node_hostname}")
                    print(f"   ℹ️  No routes added")
                    
            except Exception as e:
                logger.error(f"Failed to configure routing on {node_hostname}: {e}")
                print(f"❌ Error configuring routing on {node_hostname}: {e}")
                continue
        
        logger.info("L3 network configuration completed")
        print("\n✅ L3 network configuration completed")
        
        # Check if there are any IPv4Ext/IPv6Ext networks
        ext_networks = [name for name, info in network_info.items() 
                       if info['type'] in ["IPv4Ext", "IPv6Ext"]]
        if ext_networks:
            print("\n💡 Note: You have external networks (IPv4Ext/IPv6Ext):")
            for net_name in ext_networks:
                print(f"   - {net_name}")
            print("\n   To enable public internet routing, call:")
            print("   enable_public_routing(slice, topology)\n")
        
    except Exception as e:
        error_msg = f"Failed to configure L3 networks: {e}"
        logger.error(error_msg)
        print(f"❌ {error_msg}")
        raise SliceDeploymentError(error_msg) from e


def enable_public_routing(slice, topology: SiteTopology, networks: List[str] = None) -> None:
    """
    Enable public internet routing for IPv4Ext/IPv6Ext networks.
    
    This should be called AFTER configure_l3_networks() has completed.
    It enables public routing for external network types, making the assigned
    IPs publicly routable on the internet.
    
    Args:
        slice: FABRIC slice object (must have L3 networks already configured)
        topology: Site topology model
        networks: Optional list of specific network names to enable routing for.
                 If None, enables for all IPv4Ext/IPv6Ext networks.
        
    Raises:
        SliceDeploymentError: If public routing configuration fails
    """
    logger.info("Starting public routing configuration")
    print("\n🌍 Enabling public internet routing...\n")
    
    try:
        fab = fablib()
        needs_resubmit = False
        
        # Process each network
        for network_model in topology.site_topology_networks.iter_networks():
            # Only process external network types
            if network_model.type not in ["IPv4Ext", "IPv6Ext"]:
                continue
            
            # If specific networks requested, check if this is one of them
            if networks is not None and network_model.name not in networks:
                continue
            
            network_name = network_model.name
            logger.info(f"Enabling public routing for: {network_name} (type: {network_model.type})")
            print(f"🔧 Enabling public routing for: {network_name} ({network_model.type})")
            
            try:
                # Get the network from the slice
                fabric_network = slice.get_network(name=network_name)
                
                # Get all nodes connected to this network
                connected_nodes = topology.get_nodes_on_network(network_name)
                
                # Collect IPs that need public routing
                public_ips_to_route = []
                
                for node_model in connected_nodes:
                    try:
                        fab_node = slice.get_node(name=node_model.hostname)
                        fab_iface = fab_node.get_interface(network_name=network_name)
                        
                        # Get the assigned IP
                        node_ip = fab_iface.get_ip_addr()
                        if node_ip:
                            public_ips_to_route.append(str(node_ip))
                            logger.info(f"  Will enable routing for {node_model.hostname}: {node_ip}")
                            print(f"   📍 {node_model.hostname}: {node_ip}")
                        
                    except Exception as e:
                        logger.error(f"Failed to get IP for {node_model.hostname} on {network_name}: {e}")
                        print(f"   ⚠️  Could not get IP for {node_model.hostname}: {e}")
                        continue
                
                # Enable public routing for collected IPs
                if public_ips_to_route:
                    logger.info(f"Enabling public routing for {len(public_ips_to_route)} IPs on {network_name}")
                    print(f"   🌐 Enabling routing for {len(public_ips_to_route)} IPs...")
                    
                    try:
                        if network_model.type == "IPv4Ext":
                            fabric_network.make_ip_publicly_routable(ipv4=public_ips_to_route)
                        elif network_model.type == "IPv6Ext":
                            fabric_network.make_ip_publicly_routable(ipv6=public_ips_to_route)
                        
                        logger.info(f"Public routing enabled for {network_name}")
                        print(f"   ✅ Public routing enabled for {network_name}")
                        needs_resubmit = True
                        
                    except Exception as e:
                        logger.error(f"Failed to enable public routing for {network_name}: {e}")
                        print(f"   ❌ Failed to enable public routing: {e}")
                else:
                    logger.warning(f"No IPs found to enable routing for {network_name}")
                    print(f"   ⚠️  No IPs found for {network_name}")
                
            except Exception as e:
                logger.error(f"Failed to process network {network_name}: {e}")
                print(f"   ❌ Error processing network: {e}")
                continue
        
        # Submit changes if any external networks were configured
        if needs_resubmit:
            logger.info("Submitting slice with public routing configuration...")
            print("\n🚀 Submitting public routing configuration...")
            slice.submit()
            logger.info("Public routing configuration submitted successfully")
            print("✅ Public routing configuration submitted\n")
        else:
            logger.info("No public routing changes to submit")
            print("\nℹ️  No public routing changes were made\n")
        
    except Exception as e:
        error_msg = f"Failed to enable public routing: {e}"
        logger.error(error_msg)
        print(f"❌ {error_msg}")
        raise SliceDeploymentError(error_msg) from e


def deploy_topology_to_fabric(
    topology: SiteTopology,
    slice_name: str,
    use_timestamp: bool = False
) -> Optional[object]:
    """
    Create and submit a FABRIC slice from the provided topology.
    
    This function creates the slice infrastructure:
    1. Create nodes with components (NICs, DPUs, GPUs, FPGAs, NVMe)
    2. Apply worker constraints if specified
    3. Add post-boot commands if specified
    4. Add facility ports if defined
    5. Create and bind networks (including facility port connections)
    6. Submit slice
    
    After this, you should call:
    - configure_l3_networks(slice, topology) - for L3 network IP assignment and routing
    - enable_public_routing(slice, topology) - OPTIONAL: for IPv4Ext/IPv6Ext public access
    - configure_node_interfaces(slice, topology) - for persistent network config
    
    Args:
        topology: Site topology model
        slice_name: Proposed name for the slice
        use_timestamp: If True, ensure uniqueness by timestamping
        
    Returns:
        The created and submitted FABRIC slice, or None on failure
        
    Raises:
        SliceDeploymentError: If deployment fails
    """
    # Generate unique slice name
    unique_slice_name = check_or_generate_unique_slice_name(slice_name, use_timestamp)
    
    try:
        fab = fablib()
        slice = fab.new_slice(name=unique_slice_name)
        logger.info(f"Creating slice: {unique_slice_name}")
        print(f"\n🛠️ Creating slice: {unique_slice_name}\n")
        
        # Add nodes
        for node_id, node in topology.site_topology_nodes.nodes.items():
            logger.info(f"Adding node: {node.hostname}")
            
            # Prepare add_node arguments
            add_node_kwargs = {
                'name': node.hostname,
                'site': node.site,
                'cores': node.capacity.cpu,
                'ram': node.capacity.ram,
                'disk': node.capacity.disk,
                'image': node.capacity.os
            }
            
            
            # Add worker constraint if specified
            if node.has_worker_constraint():
                add_node_kwargs['host'] = node.worker
                logger.info(f"Node {node.hostname} constrained to worker: {node.worker}")
                print(f"   📍 Placing {node.hostname} on worker: {node.worker}")
            
            # Create the node
            fab_node = slice.add_node(**add_node_kwargs)
            
            # Add components
            add_gpus_to_node(fab_node, node)
            add_dpus_to_node(fab_node, node)
            add_fpgas_to_node(fab_node, node)
            add_nvmes_to_node(fab_node, node)
            add_nics_to_node(fab_node, node)
            add_persistent_storage_to_node(fab_node, node)
            
            # Add post-boot commands if specified
            add_postboot_commands_to_node(fab_node, node)
        
        # Add facility ports if defined
        facility_port_objects = add_facility_ports_to_slice(topology, slice)
        
        # Create and bind networks (handles NICs, DPUs, FPGAs, and facility ports)
        create_and_bind_networks(topology, slice, facility_port_objects)
        
        # Submit slice
        logger.info("Submitting slice to FABRIC...")
        print("\n🚀 Submitting slice...")
        slice.submit()
        
        # At the end, update the print statement:
        logger.info(f"Slice '{unique_slice_name}' submitted successfully")
        print(f"✅ Slice '{unique_slice_name}' created successfully")
        print(f"\n💡 Next steps:")
        print(f"   1. Call configure_l3_networks(slice, topology) for L3 IP assignment")
        print(f"   2. [Optional] Call enable_public_routing(slice, topology) for IPv4Ext/IPv6Ext")
        print(f"   3. Call configure_node_interfaces(slice, topology) for persistent config")
    
        return slice       

    except Exception as e:
        error_msg = f"Failed to deploy slice: {e}"
        logger.critical(error_msg)
        print(f"❌ {error_msg}")
        raise SliceDeploymentError(error_msg) from e


def get_slice(slice_name: str) -> Optional[object]:
    """
    Retrieve an existing FABRIC slice by name.
    
    Args:
        slice_name: Name of the slice to retrieve
        
    Returns:
        FABRIC slice object, or None if not found
    """
    try:
        fab = fablib()
        logger.info(f"Retrieving slice: {slice_name}")
        print(f"\n🚀 Getting slice object for: '{slice_name}'")
        
        slice = fab.get_slice(name=slice_name)
        print(f"✅ Slice retrieved successfully")
        return slice
        
    except Exception as e:
        logger.error(f"Failed to retrieve slice '{slice_name}': {e}")
        print(f"❌ Exception: {e}")
        return None


def delete_slice(slice_name: str) -> bool:
    """
    Delete a FABRIC slice.
    
    Args:
        slice_name: Name of the slice to delete
        
    Returns:
        True if deletion successful, False otherwise
    """
    try:
        fab = fablib()
        slice = fab.get_slice(name=slice_name)
        
        logger.info(f"Deleting slice: {slice_name}")
        print(f"\n🚀 Deleting slice '{slice_name}'")
        
        slice.delete()
        logger.info(f"Slice '{slice_name}' deleted successfully")
        print(f"✅ Slice '{slice_name}' deleted")
        return True
        
    except Exception as e:
        logger.error(f"Failed to delete slice '{slice_name}': {e}")
        print(f"❌ Slice deletion failed: {e}")
        return False


def check_slices() -> None:
    """Display all existing slices."""
    try:
        fab = fablib()
        print(f"\n🚀 Checking existing slices...")
        
        slices = list(fab.get_slices())
        if not slices:
            print("No slices found")
            return
        
        for slice in slices:
            print(f"✅ {slice}")
            
    except Exception as e:
        logger.error(f"Failed to check slices: {e}")
        print(f"❌ Exception: {e}")


def show_config() -> None:
    """Display current Fablib configuration."""
    try:
        fab = fablib()
        print(f"\n🚀 Fablib Configuration:")
        fab.show_config()
        print(f"✅ Configuration displayed")
        
    except Exception as e:
        logger.error(f"Failed to show config: {e}")
        print(f"❌ Exception: {e}")
