from itertools import chain
from typing import List, Optional
from pydantic import BaseModel
from vrouter.client import ExtendedVPPAPIClient
from vrouter.common import models
from loguru import logger as log
import ipaddress
from vrouter_agent.utils.cli import run_command
from vrouter_agent.utils.config import convert_wg_to_gre_ip_addr
from vrouter_agent.utils.gre_manager import get_gre_manager, convert_wg_to_gre_ip_addr_safe
from vrouter_agent.models import Tunnel
from vrouter_agent.core.base import CreatedWireguardTunnel, Interface
from vrouter_agent.core.config import settings
from vrouter_agent.services.frr import FRR
from vrouter_agent.schemas.tunnel_config import WireGuardConfig

    
class VRouterClient:
    """
    A class to manage VPP API client operations.
    """
    
    def __init__(self, tunnels: List[WireGuardConfig], frr_config:Optional[str]=None, lcp_global: str = "frr"):
        self.client = ExtendedVPPAPIClient()
        self.tunnels = tunnels
        self.wireguard_tunnels: Optional[List[CreatedWireguardTunnel]] = None
        self.gre_tunnels = None
        self.lcp_global = lcp_global
        self.frr_config = frr_config
        
        
    def is_connected(self):
        """
        Check if the client is connected to the vRouter.
        vRouter is the VPP agent.
        Returns:
            bool: True if connected, False otherwise.
        """
        try:
            self.client.get_configuration()
            self.add_lcp_global_configuration()
            return True
        except Exception as e:
            log.error(f"Error connecting to vRouter: {e}")
            return False
    def _get_next_available_wg_index(self, start_index: int = 0) -> int:
        """
        Get the next available WireGuard interface index by checking existing interfaces.
        Args:
            start_index (int): The starting index to check from.
        Returns:
            int: The next available index for WireGuard interface naming.
        """
        try:
            current_config = self.client.get_configuration()
            existing_wg_names = set()
            
            # Extract existing WireGuard interface names
            for item in current_config.items:
                if hasattr(item.config, 'name') and hasattr(item.config, 'type'):
                    if (hasattr(models.InterfaceType, 'WIREGUARD_TUNNEL') and 
                        item.config.type == models.InterfaceType.WIREGUARD_TUNNEL):
                        existing_wg_names.add(item.config.name)
            
            # Find the next available index
            index = start_index
            while f"wg{index}" in existing_wg_names:
                index += 1
                
            log.debug(f"Next available WireGuard index: {index}, existing interfaces: {existing_wg_names}")
            return index
            
        except Exception as e:
            log.warning(f"Could not check existing WireGuard interfaces: {e}, starting from index {start_index}")
            return start_index

    def create_wireguard_tunnels(self) -> dict:
        """
        Create wireguard tunnels.
        Returns:
            dict: Contains 'success' boolean and 'interfaces' list with Django Interface model data
        """
        wireguard_interfaces = []
        wireguard_peers = []
        routes= []
        log.debug(f"Creating wireguard tunnels: {self.tunnels}")
        
        # Get the starting index for new WireGuard interfaces
        current_wg_index = self._get_next_available_wg_index()
        created_tunnels = []
        created_interfaces_data = []  # For Django Interface model data
        
        for tunnel in self.tunnels:
            wg_name = f"wg{current_wg_index}"
            log.debug(f"Creating WireGuard tunnel with name: {wg_name}")
            wireguard_interfaces.append(
                models.InterfaceConfigurationItem(
                    name=wg_name,
                    type=models.InterfaceType.WIREGUARD_TUNNEL,
                    enabled=True,
                    ip_addresses=[f"{tunnel.address}/30"],
                    mtu= tunnel.mtu if tunnel.mtu else 1420,  # default mtu is 1420
                    link=models.WireguardInterfaceLink(
                        private_key=tunnel.private_key,
                        port=tunnel.listen_port,
                        src_addr=ipaddress.IPv4Address(tunnel.source_ip) if tunnel.source_ip else None,
                    ),
                )
            )
            wireguard_peers.append(
                models.WireguardPeerConfigurationItem(
                    wg_if_name=wg_name,
                    public_key=tunnel.peer_public_key,
                    allowed_ips=["0.0.0.0/0"],
                    persistent_keepalive=tunnel.persistent_keepalive,
                    endpoint=ipaddress.IPv4Address(tunnel.peer_endpoint.split(":")[0]) if tunnel.peer_endpoint else None,
                    port=tunnel.peer_endpoint.split(":")[1] if tunnel.peer_endpoint and ":" in tunnel.peer_endpoint else None,
                )
            )
            
            routes.append(
                models.RouteConfigurationItem(
                    destination_network=f"{tunnel.peer_address.split('/')[0]}/32",
                    next_hop_address=tunnel.address.split("/")[0],
                    outgoing_interface=wg_name,
                )
            )
            created_tunnels.append(
                CreatedWireguardTunnel(
                    name=wg_name,  # Use the actual VPP interface name
                    ip_address=tunnel.address,
                    peer_ip_address=tunnel.peer_address,
                    mapped_name=tunnel.interface_name if tunnel.interface_name else None,  # Use the provided interface name if available
                )
            )
            
            # Create Django Interface model data
            ip_address_without_cidr = tunnel.address.split('/')[0]
            subnet_mask = int(tunnel.address.split('/')[1]) if '/' in tunnel.address else 30
            
            interface_data = {
                'name': wg_name,
                'type': 'tunnel',
                'up': True,  # Will be set to True initially, can be verified later
                'hwaddr': None,  # WireGuard interfaces don't have MAC addresses
                'ip_address': ip_address_without_cidr,
                'subnet_mask': subnet_mask,
                'vlan_id': None,
                'mtu': tunnel.mtu if tunnel.mtu else 1420,
                'vpp_used': True,
                'vpp_interface_name': wg_name,
                'is_enabled': True,
                'description': f"WireGuard tunnel to {tunnel.peer_endpoint}",
                'is_management': False,
                'is_wan': True,  # WireGuard tunnels are typically WAN connections
                'is_lan': False,
                'is_primary': False,
                # Additional tunnel-specific metadata
                'tunnel_metadata': {
                    'tunnel_type': 'wireguard',
                    'peer_public_key': tunnel.peer_public_key,
                    'peer_endpoint': tunnel.peer_endpoint,
                    'peer_address': tunnel.peer_address,
                    'listen_port': tunnel.listen_port,
                    'persistent_keepalive': tunnel.persistent_keepalive
                }
            }
            created_interfaces_data.append(interface_data)
            
            current_wg_index += 1
            

        # Apply all configurations at once outside the loop
        if wireguard_interfaces or wireguard_peers or routes:
            reply = self.client.add_configuration(
                *chain(wireguard_interfaces, wireguard_peers, routes)
            )
            log.debug(reply)
            
         
            if reply and reply.processing_error:
                log.error(f"Error adding wireguard tunnels: {reply.processing_error}")
                return {
                    'success': False,
                    'interfaces': [],
                    'error': reply.processing_error
                }
            
            if reply and reply.all_added_items_applied_to_vpp:
                self.wireguard_tunnels = created_tunnels
                log.info(f"Wireguard tunnels created successfully: {[t.name for t in created_tunnels]}")
                return {
                    'success': True,
                    'interfaces': created_interfaces_data,
                    'created_tunnels': [t.name for t in created_tunnels]
                }
            else:
                log.error(f"Failed to create wireguard tunnels")
                return {
                    'success': False,
                    'interfaces': [],
                    'error': 'Failed to apply configuration to VPP'
                }
        else:
            log.warning("No WireGuard tunnels to create")
            return {
                'success': True,
                'interfaces': [],
                'message': 'No tunnels to create'
            }


    def remove_wireguard_tunnels(self) -> bool:
        """
        Remove all WireGuard tunnels.
        Returns:
            bool: True if tunnels were removed successfully, False otherwise.
        """
        if not self.wireguard_tunnels:
            log.warning("No WireGuard tunnels to remove")
            return True
        log.debug(f"Removing WireGuard tunnels: {[t.name for t in self.wireguard_tunnels]}")
        try:
            current_config = self.client.get_configuration()
            configs = []
            
            for tunnel in self.wireguard_tunnels:
                #Find the WireGuard interface configuration
                interface_config = [
                    item.config
                    for item in current_config.items
                    if isinstance(item.config, models.InterfaceConfigurationItem)
                    and item.config.name == tunnel.name
                ]
                if not interface_config:
                    log.error(f"Wireguard interface {tunnel.name} not found")
                    
      
                peer_config = [ item.config  for item in current_config.items if isinstance(item.config, models.WireguardPeerConfigurationItem) and item.config.wg_if_name == tunnel.name]
                if not peer_config:
                    log.error(f"Wireguard peer for interface {tunnel.name} not found")
                    
                
                route_config = [
                    item.config
                    for item in current_config.items
                    if isinstance(item.config, models.RouteConfigurationItem)
                    and item.config.outgoing_interface == tunnel.name
                ]
                if not route_config:
                    log.info(f"Route for Wireguard interface {tunnel.name} not found")
                    
                configs.extend(interface_config + peer_config + route_config)
            if not configs:
                log.warning("No WireGuard configurations found to remove")
                return True
            
            log.debug(f"Configurations to remove: {configs}")
               
            # Remove the WireGuard interface, peer, and route if they exist
            reply = self.client.delete_configuration(*configs)
            log.debug(reply)

            self.wireguard_tunnels = []
            log.info("WireGuard tunnels removed successfully")
            return True
        except Exception as e:
            log.error(f"Exception while removing WireGuard tunnels: {e}")
            return False
        
    def create_gre_tunnels(self) -> List[CreatedWireguardTunnel]:
        """
        Create GRE tunnels based on the WireGuard tunnels.
        Returns:
            List[CreatedWireguardTunnel]: A list of created GRE tunnels.
        """
        if not self.wireguard_tunnels:
            log.error("No WireGuard tunnels created yet")
            return []

        self.gre_tunnels = []
        for i, wg_tunnel in enumerate(self.wireguard_tunnels):
            # Find the corresponding original tunnel config to get GRE fields
            original_tunnel = None
            if i < len(self.tunnels):
                original_tunnel = self.tunnels[i]
            else:
                # Fallback: find by interface name matching
                for tunnel_config in self.tunnels:
                    if tunnel_config.interface_name == wg_tunnel.mapped_name:
                        original_tunnel = tunnel_config
                        break
            log.debug(f"Creating GRE tunnel for WireGuard tunnel: {wg_tunnel.name}, original tunnel: {original_tunnel}")
            gre_name, gre_ip = self.add_wireguard_gre_tunnel(
                wg_tunnel,
                original_tunnel
            )
            self.gre_tunnels.append(
                CreatedWireguardTunnel(name=gre_name, ip_address=gre_ip)
            )
        return self.gre_tunnels
    
    def create_ospf_configuration(self):
        
        # primary_lan = [intf for intf in settings.config.interfaces if intf.is_primary and intf.type == "lan"][0]
        # # expose the primary LAN interface to the FRR container
        # self.expose_lan_interface_to_frr_container(
        #     primary_lan.interface_name,
        #     primary_lan.ip_address,
        #     primary_lan.prefix_len,
            
        # )
        # add bgp loopback so that the FRR container can communicate with the route reflector
        self.add_bgp_loopback(
            settings.config.frr.loopback.split("/")[0]
        )
        if self.ospf_config.client_enabled:
            self.add_ospf_lcp_fix(
                self.ospf_config.lan_ip_address,
                primary_lan.interface_name
            )
            self.add_client_neighbour_info(
                primary_lan.interface_name,
                self.ospf_config.lan_ip_address,
                self.ospf_config.lan_mac_address,
            )
        
        frr = FRR(self.ospf_config, self.wireguard_tunnels )
        frr.apply_all_configs()

        # self.add_routes_for_bidirectional_bgp_communication(
        #     "frr",
        #     settings.config.frr.loopback.split("/")[0],
        #     settings.config.frr.controller_bgp.loopback.split("/")[0],
        #     settings.config.frr.controller_bgp.peer_ip.split("/")[0],
        # )
        
        # self.add_nat_configuration(
        #     lan_iface=settings.config.interfaces["lan_primary"],
        #     wan_iface=settings.config.interfaces["wan_primary"],
        #     wg_ports_to_open=list(
        #         set([tunnel.source_on_host.listen_port for tunnel in self.tunnels])
        #     ),
        #     tcp_ports=[
        #         22,
        #         2206,
        #         2205,
        #     ],  # TODO: Add the ports from the order
        #     udp_ports=[settings.config.route_reflector.port],
        # )
        # self.add_acl_configuration(
        #     ingress_iface=settings.config.interfaces["lan_primary"],
        #     deny_network="8.8.4.4/32",
        # )


    def apply_frr_configuration(self):
        """
        Apply the FRR configuration to the vRouter agent.
        Returns:
            bool: True if the FRR configuration was applied successfully, False otherwise.
        """
        if not self.frr_config:
            log.error("FRR configuration is not set")
            return False
        
        reply = self.client.add_configuration(
            models.FRRConfigurationItem(
                config=self.frr_config,
            )
        )
        log.debug(reply)
        if reply.processing_error:
            log.error(f"Error applying FRR configuration: {reply.processing_error}")
            return False
        return True

        
    def add_lcp_global_configuration(self):
        """
        Add a global LCP configuration to the vRouter agent.
        Args:
            lcp_interface_name (str): The name of the LCP interface.
        Returns:
            bool: True if the LCP configuration was added successfully, False otherwise.
        """
        # Check if the LCP global configuration already exists
        current_config = self.client.get_configuration()
        lcp_config = [
            item.config
            for item in current_config.items
            if isinstance(item.config, models.LCPGlobalsConfigurationItem)
            and item.config.default_namespace == self.lcp_global
        ]
        if lcp_config:
            log.debug("LCP global configuration already exists. Skipping addition.")
            return False
        # Add the LCP global configuration

        reply = self.client.add_configuration(
            models.LCPGlobalsConfigurationItem(
                default_namespace=self.lcp_global,
                lcp_sync=True,
                lcp_auto_subint=True,
            )
        )
        log.debug(reply)
        if reply.processing_error:
            log.error(
                f"Error adding LCP global configuration: {reply.processing_error}"
            )
            return False
        log.info("LCP global configuration added successfully")
        return True

    def delete_lcp_global_configuration(self, namespace: str):
        """
        Delete the global LCP configuration from the vRouter agent.
        Args:
            client: The client object used to communicate with the server.
        Returns:
            bool: True if the LCP configuration was deleted successfully, False otherwise.
        """

        current_config = self.client.get_configuration()
        lcp_config = [
            item.config
            for item in current_config.items
            if isinstance(item.config, models.LCPGlobalsConfigurationItem)
            and item.config.default_namespace == namespace
        ]
        if not lcp_config:
            log.error("LCP global configuration not found")
            return False
        reply = self.client.delete_configuration(lcp_config[0])
        if reply.processing_error:
            log.error(
                f"Error deleting LCP global configuration: {reply.processing_error}"
            )
            return False
        return True

    def add_bgp_loopback(self, ip_addr: str):
        """
        Add a BGP loopback interface to the client.
        Args:
            client: The client object used to communicate with the server.
            ip_addr (str): The IP address of the loopback interface.
        Returns:
            bool: True if the loopback interface was added successfully, False otherwise.
        """

        reply = self.client.add_configuration(
            models.InterfaceConfigurationItem(
                name="loop0",
                type=models.InterfaceType.SOFTWARE_LOOPBACK,
                enabled=True,
                ip_addresses=[ip_addr + "/32"],
            )
        )
        log.debug(reply)
        if reply.processing_error:
            log.error(f"Error adding loopback interface: {reply.processing_error}")
            return False
        return True

    
    def add_vpp_route(self, dest_net, next_hop_addr, outgoing_interface):
        reply = self.client.add_configuration(
            models.RouteConfigurationItem(
                destination_network=dest_net,
                next_hop_address=next_hop_addr,
                outgoing_interface=outgoing_interface,
            )
        )
        log.debug(reply)
        if reply.processing_error:
            log.error(f"Error adding route: {reply.processing_error}")
            return False
        return True

    def add_linux_route(
        self,
        dest_net: str,
        next_hop_addr: str = "",
        outgoing_iface: str = "",
        container: str = "",  # empty container name means default linux namespace
    ):
        reply = self.client.add_configuration(
            models.LinuxRouteConfigurationItem(
                docker_container_name=container,
                destination_network=dest_net,
                next_hop_address=next_hop_addr,
                outgoing_interface=outgoing_iface,
            )
        )
        log.debug(reply)
        if reply.processing_error:
            log.error(f"Error adding route: {reply.processing_error}")
            return False
        return True

 
    def ping(self, destination_ip: str):
        cmd = f"sudo vppctl ping {destination_ip.split('/')[0]} repeat 1"
        res, _ = run_command(cmd.split())
        log.debug(res)
        if "1 sent, 1 received" in res:
            log.info(f"Ping to {destination_ip} successful")
            return True
        log.error(f"Ping to {destination_ip} failed")
        return False

    def check_interface_status(self, interface_name: str) -> bool:
        """
        Check if a VPP interface is up and operational.
        
        Args:
            interface_name (str): Name of the interface to check
            
        Returns:
            bool: True if interface is up, False otherwise
        """
        try:
            cmd = f"sudo vppctl show interface {interface_name}"
            res, _ = run_command(cmd.split())
            log.debug(f"Interface status for {interface_name}: {res}")
            
            # Check if interface is listed and not down
            if interface_name in res and "up" in res.lower() and "down" not in res.lower():
                log.info(f"Interface {interface_name} is up")
                return True
            else:
                log.warning(f"Interface {interface_name} is down or not found")
                return False
                
        except Exception as e:
            log.error(f"Error checking interface {interface_name} status: {e}")
            return False

    def get_interface_configuration(self, interface_name: str) -> Optional[dict]:
        """
        Get detailed configuration information for a VPP interface.
        
        Args:
            interface_name (str): Name of the interface
            
        Returns:
            Optional[dict]: Interface configuration details or None if not found
        """
        try:
            current_config = self.client.get_configuration()
            
            for item in current_config.items:
                if hasattr(item.config, 'name') and item.config.name == interface_name:
                    return {
                        'name': item.config.name,
                        'type': str(item.config.type),
                        'enabled': getattr(item.config, 'enabled', None),
                        'ip_addresses': getattr(item.config, 'ip_addresses', []),
                        'mtu': getattr(item.config, 'mtu', None)
                    }
            
            log.warning(f"Interface {interface_name} not found in configuration")
            return None
            
        except Exception as e:
            log.error(f"Error getting interface {interface_name} configuration: {e}")
            return None

    def test_tunnel_connectivity(self, tunnel_name: str, peer_address: str, retries: int = 3) -> bool:
        """
        Test connectivity through a tunnel by checking interface status and ping.
        
        Args:
            tunnel_name (str): Name of the tunnel interface
            peer_address (str): Peer IP address to ping (without CIDR)
            retries (int): Number of ping attempts
            
        Returns:
            bool: True if tunnel is operational (interface up + ping successful)
        """
        try:
            log.info(f"Testing connectivity for tunnel {tunnel_name} to {peer_address}")
            
            # First check if interface is up
            if not self.check_interface_status(tunnel_name):
                log.error(f"Tunnel {tunnel_name} interface is not up")
                return False
            
            # Test connectivity with retries
            for attempt in range(retries):
                if self.ping(peer_address):
                    log.info(f"Tunnel {tunnel_name} connectivity test successful (attempt {attempt + 1})")
                    return True
                
                if attempt < retries - 1:  # Don't sleep on last attempt
                    log.debug(f"Ping attempt {attempt + 1} failed, retrying...")
                    import time
                    time.sleep(1)
            
            log.error(f"Tunnel {tunnel_name} connectivity test failed after {retries} attempts")
            return False
            
        except Exception as e:
            log.error(f"Error testing tunnel {tunnel_name} connectivity: {e}")
            return False

    def verify_tunnels_operational(self) -> List[dict]:
        """
        Verify all created WireGuard tunnels are operational.
        
        Returns:
            List[dict]: List of tunnel status information
        """
        results = []
        
        if not self.wireguard_tunnels:
            log.warning("No WireGuard tunnels to verify")
            return results
        
        log.info(f"Verifying {len(self.wireguard_tunnels)} WireGuard tunnels")
        
        for tunnel in self.wireguard_tunnels:
            try:
                # Get corresponding tunnel config to find peer address
                tunnel_config = None
                for config in self.tunnels:
                    if config.address.split('/')[0] in tunnel.ip_address:
                        tunnel_config = config
                        break
                
                if not tunnel_config:
                    log.error(f"Could not find config for tunnel {tunnel.name}")
                    results.append({
                        'tunnel_name': tunnel.name,
                        'status': 'error',
                        'interface_up': False,
                        'connectivity': False,
                        'error': 'Configuration not found'
                    })
                    continue
                
                # Check interface status
                interface_up = self.check_interface_status(tunnel.name)
                
                # Test connectivity to peer
                peer_ip = tunnel_config.peer_address.split('/')[0]
                connectivity = False
                if interface_up:
                    connectivity = self.test_tunnel_connectivity(tunnel.name, peer_ip)
                
                status = 'active' if (interface_up and connectivity) else 'inactive'
                
                results.append({
                    'tunnel_name': tunnel.name,
                    'status': status,
                    'interface_up': interface_up,
                    'connectivity': connectivity,
                    'peer_ip': peer_ip,
                    'local_ip': tunnel.ip_address
                })
                
                log.info(f"Tunnel {tunnel.name} verification: {status} (interface: {interface_up}, connectivity: {connectivity})")
                
            except Exception as e:
                log.error(f"Error verifying tunnel {tunnel.name}: {e}")
                results.append({
                    'tunnel_name': tunnel.name,
                    'status': 'error',
                    'interface_up': False,
                    'connectivity': False,
                    'error': str(e)
                })
        
        return results

    def get_wireguard_interfaces(self) -> List[dict]:
        """
        Get list of WireGuard interfaces from VPP configuration.
        
        Returns:
            List[dict]: List of WireGuard interface information
        """
        try:
            # Ensure we're connected before making the call
            if not self.is_connected():
                log.warning("VPP client not connected, attempting to reconnect")
                return []
                
            current_config = self.client.get_configuration()
            wireguard_interfaces = []
            
            for item in current_config.items:
                if (hasattr(item.config, 'type') and 
                    hasattr(models.InterfaceType, 'WIREGUARD_TUNNEL') and
                    item.config.type == models.InterfaceType.WIREGUARD_TUNNEL):
                    
                    iface_info = {
                        'name': item.config.name,
                        'enabled': getattr(item.config, 'enabled', False),
                        'ip_addresses': getattr(item.config, 'ip_addresses', []),
                        'mtu': getattr(item.config, 'mtu', None)
                    }
                    
                    # Extract WireGuard-specific info if available
                    if hasattr(item.config, 'link') and item.config.link:
                        link = item.config.link
                        iface_info.update({
                            'private_key': getattr(link, 'private_key', None),
                            'listen_port': getattr(link, 'port', None),
                            'src_addr': str(getattr(link, 'src_addr', None)) if getattr(link, 'src_addr', None) else None
                        })
                    
                    wireguard_interfaces.append(iface_info)
            
            log.debug(f"Found {len(wireguard_interfaces)} WireGuard interfaces: {[iface['name'] for iface in wireguard_interfaces]}")
            return wireguard_interfaces
            
        except Exception as e:
            log.error(f"Error getting WireGuard interfaces: {e}")
            return []
    
    def get_wireguard_peers(self) -> List[dict]:
        """
        Get list of WireGuard peers from VPP configuration.
        
        Returns:
            List[dict]: List of WireGuard peer information
        """
        try:
            # Ensure we're connected before making the call
            if not self.is_connected():
                log.warning("VPP client not connected, attempting to reconnect")
                return []
                
            current_config = self.client.get_configuration()
            wireguard_peers = []
            
            for item in current_config.items:
                if isinstance(item.config, models.WireguardPeerConfigurationItem):
                    peer_info = {
                        'interface': item.config.wg_if_name,
                        'public_key': item.config.public_key,
                        'allowed_ips': getattr(item.config, 'allowed_ips', []),
                        'endpoint': str(item.config.endpoint) if item.config.endpoint else None,
                        'port': item.config.port,
                        'persistent_keepalive': getattr(item.config, 'persistent_keepalive', None)
                    }
                    wireguard_peers.append(peer_info)
            
            log.debug(f"Found {len(wireguard_peers)} WireGuard peers")
            return wireguard_peers
            
        except Exception as e:
            log.error(f"Error getting WireGuard peers: {e}")
            return []

    def verify_tunnels_operational_enhanced(self, interface_names: Optional[List[str]] = None) -> dict:
        """
        Enhanced version of verify_tunnels_operational that can verify specific interfaces.
        
        Args:
            interface_names: List of interface names to verify. If None, verify all created tunnels.
        
        Returns:
            dict: Mapping of interface names to their verification results
        """
        results = {}
        
        # If specific interface names provided, verify those
        if interface_names:
            log.info(f"Verifying specified WireGuard interfaces: {interface_names}")
            
            for interface_name in interface_names:
                try:
                    # Check interface status
                    interface_up = self.check_interface_status(interface_name)
                    
                    # Get interface configuration to find peer info
                    iface_config = self.get_interface_configuration(interface_name)
                    connectivity_test = False
                    
                    if interface_up and iface_config:
                        # Try to find peer address for connectivity test
                        peers = self.get_wireguard_peers()
                        interface_peers = [p for p in peers if p.get('interface') == interface_name]
                        
                        if interface_peers:
                            # For now, mark connectivity as True if interface is up and has peers
                            # More sophisticated connectivity testing could be added here
                            connectivity_test = True
                    
                    operational = interface_up and connectivity_test
                    
                    results[interface_name] = {
                        'operational': operational,
                        'interface_up': interface_up,
                        'connectivity_test': connectivity_test
                    }
                    
                    log.info(f"Interface {interface_name} verification: operational={operational} "
                           f"(interface_up={interface_up}, connectivity={connectivity_test})")
                    
                except Exception as e:
                    log.error(f"Error verifying interface {interface_name}: {e}")
                    results[interface_name] = {
                        'operational': False,
                        'interface_up': False,
                        'connectivity_test': False,
                        'error': str(e)
                    }
            
            return results
        
        # Fallback to original behavior if no specific interfaces provided
        original_results = self.verify_tunnels_operational()
        
        # Convert to new format
        for result in original_results:
            tunnel_name = result.get('tunnel_name')
            if tunnel_name:
                operational = result.get('status') == 'active'
                results[tunnel_name] = {
                    'operational': operational,
                    'interface_up': result.get('interface_up', False),
                    'connectivity_test': result.get('connectivity', False)
                }
                if 'error' in result:
                    results[tunnel_name]['error'] = result['error']
        
        return results


    def add_ospf_lcp_fix(self, client_vpp_intf_address, vpp_lan_intf):
        # https://lists.fd.io/g/vpp-dev/topic/83103366#19478
        # for 224.0.0.5 and 224.0.0.6 used by OSPF (https://en.wikipedia.org/wiki/Open_Shortest_Path_First)

        # run_command(["sudo", "vppctl", "ip", "mroute", "add", "224.0.0.0/24", "via", "local", "Forward"])
        # for i in range(4):
        #     run_command(["sudo", "vppctl", "ip", "mroute", "add", "224.0.0.0/24", "via", os.environ[os.environ["VPP"]+f"_WG{i}_INTERFACE_NAME"], "Accept"])

        # to steer OSPF Hello packets from FRR container to client
        self.add_vpp_route(
            "224.0.0.0/24",  # static due to OSPF hello packets always destined for 224.0.0.5
            client_vpp_intf_address,
            vpp_lan_intf,
        )

        # to get ospf hello packets from client to frr container through vpp
        run_command(
            [
                "sudo",
                "vppctl",
                "ip",
                "mroute",
                "add",
                client_vpp_intf_address,
                "224.0.0.5",  # static destination ip address of Hello OSPF packets, fixed by OSPF protocol itself
                "via",
                vpp_lan_intf,
                "Accept",
            ]
        )
        run_command(
            [
                "sudo",
                "vppctl",
                "ip",
                "mroute",
                "add",
                client_vpp_intf_address,
                "224.0.0.5",  # static destination ip address of Hello OSPF packets, fixed by OSPF protocol itself
                "via",
                "local",  # static, set to be locally processed (processed by LCP)
                "Forward",
            ]
        )

    # Note: this does not include RR-node wireguard tunnel (for that see makefile) and RR setup => only path from linux WG
    # on node to BGP in container on node
    def add_routes_for_bidirectional_bgp_communication(
        self,
        local_bgp_loop_ip: str,
        rr_bgp_loop_ip: str,
        rr_wg_ip: str,
        frr_container_name: str = "frr"
    ):
        # Some path explanation:
        # RR-to-node path: node-linux-wg0 -(adding here route1+iptables forwarding)-> FRR container default interface ->
        #   FRR container routing to local loopback -(ping echo going back, need to add route2)-> FRR container default
        #   interface -(routing path was added as part of wg configuration)-> node-linux-wg0
        # Node-to-RR path: FRR instance in FRR container -(need to add route3)-> FRR container default interface
        #   -> node linux host route(actually controlled by WG allowed ip configuration) -> wg tunnel to RR
        host_ip_from_docker_container = "172.17.0.1"

        # adding route1
        frr_container_ip_address, _ = run_command(
            [
                "docker",
                "inspect",
                "-f",
                "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'",
                frr_container_name,
            ]
        )
        frr_container_ip_address = frr_container_ip_address.replace("\n", "").replace(
            "'", ""
        )
        log.debug(f"FRR container IP address: {frr_container_ip_address}")

        self.add_linux_route(
            local_bgp_loop_ip,
            frr_container_ip_address,
        )
        run_command(
            [
                "sudo",
                "iptables",
                "-A",
                "FORWARD",
                "-i",
                "wg0",  # static for default wireguard interface between controller and node 
                "-o",
                "docker0",  # static for default docker installation
                "-j",
                "ACCEPT",
            ]
        )
        # adding route2
        self.add_linux_route(
            rr_wg_ip,
            host_ip_from_docker_container,
            "",
            frr_container_name,
        )
        # adding route3
        self.add_linux_route(
            rr_bgp_loop_ip,
            host_ip_from_docker_container,
            "",
            frr_container_name,
        )

    def add_client_neighbour_info(
        self, vpp_lan_intf: str, client_vpp_intf_ip: str, client_vpp_intf_mac: str
    ):

        # using MAC address of client interface (or at least what TREX traffic destination MAC address that VPP must
        # steer back to Trex through client interface)
        cmd = f"sudo vppctl ip neighbor {vpp_lan_intf} {client_vpp_intf_ip} {client_vpp_intf_mac}"
        _, ret = run_command(cmd.split())
        if ret == 0:
            log.info(f"Added neighbour info for {client_vpp_intf_ip}")
        else:

            log.error(f"Error adding neighbour info for {client_vpp_intf_ip}")
            return False
        return True

    def add_wireguard_gre_tunnel(
        self,
        tunnel: CreatedWireguardTunnel,
        original_tunnel_config: Optional[WireGuardConfig] = None,
    ):
        """
        Add a GRE tunnel for a WireGuard tunnel using predefined GRE configuration or fallback to automatic generation.
        
        Args:
            tunnel: WireGuard tunnel configuration
            original_tunnel_config: Original tunnel configuration containing GRE fields
            
        Returns:
            Tuple of (gre_interface_name, gre_interface_ip_with_mask)
        """
        try:
            # Check if we have predefined GRE configuration
            if (original_tunnel_config and 
                original_tunnel_config.gre_name and 
                original_tunnel_config.gre_local_tunnel_ip and 
                original_tunnel_config.gre_remote_tunnel_ip):
                
                # Use predefined GRE configuration from tunnel data
                gre_interface_name = original_tunnel_config.gre_name
                gre_interface_ip_address = original_tunnel_config.gre_local_tunnel_ip
                remote_gre_interface_ip_address = original_tunnel_config.gre_remote_tunnel_ip
                
                # Determine the subnet mask - default to /30 for GRE point-to-point links
                if "/" in gre_interface_ip_address:
                    local_gre_ip_with_mask = gre_interface_ip_address
                    gre_interface_ip_address = gre_interface_ip_address.split("/")[0]
                    gre_interface_ip_address_mask = local_gre_ip_with_mask.split("/")[1]
                else:
                    # Default to /30 for point-to-point GRE tunnels
                    gre_interface_ip_address_mask = "30"
                    local_gre_ip_with_mask = f"{gre_interface_ip_address}/{gre_interface_ip_address_mask}"
                
                log.info(
                    f"Using predefined GRE configuration - Interface: {gre_interface_name}, "
                    f"Local: {local_gre_ip_with_mask}, Remote: {remote_gre_interface_ip_address}, "
                    f"WG Tunnel: {tunnel.name} ({tunnel.ip_address})"
                )
            else:
                # Fallback to automatic generation using GRE manager
                log.info(f"No predefined GRE configuration found for {tunnel.name}, using automatic generation")
                
                # Extract index from tunnel name (e.g., "wg0" -> "0")
                if tunnel.name.startswith("wg"):
                    index = tunnel.name[2:]  # Remove "wg" prefix to get the numeric index
                else:
                    # Fallback to old method for backward compatibility
                    index = tunnel.name.split("_")[-1]
                
                gre_interface_name = f"gre{index}"
                
                # Use the new GRE manager to allocate addresses safely
                gre_manager = get_gre_manager()
                local_gre_ip_with_mask, remote_gre_interface_ip_address = convert_wg_to_gre_ip_addr_safe(tunnel, gre_manager)
                
                # Extract components for processing
                gre_interface_ip_address = local_gre_ip_with_mask.split("/")[0]
                gre_interface_ip_address_mask = local_gre_ip_with_mask.split("/")[1]
                
                log.info(
                    f"GRE tunnel allocation (auto-generated) - Interface: {gre_interface_name}, "
                    f"Local: {local_gre_ip_with_mask}, Remote: {remote_gre_interface_ip_address}, "
                    f"WG Tunnel: {tunnel.name} ({tunnel.ip_address})"
                )

            def add_frr_agent_ip_translation_from_gre_to_wg(
                gre_interface_ip_address: str,
                interface_ip: str,
                remote_gre_interface_ip_address: str,
                remote_tunnel_end_ip: str,
                frr_container_name: str = "frr",
                replace_file_path: str = "/rr-ip-replace.txt",
            ):
                # TODO when proper python client API will be created for this then replace this with python client code
                _, code1 = run_command(
                    [
                        "docker",
                        "exec",
                        "-t",
                        frr_container_name,
                        "sh",
                        "-c",
                        "echo '"
                        + gre_interface_ip_address
                        + "=>"
                        + interface_ip
                        + "' >> "
                        + replace_file_path,
                    ]
                )
                _, code2 = run_command(
                    [
                        "docker",
                        "exec",
                        "-t",
                        frr_container_name,
                        "sh",
                        "-c",
                        "echo '"
                        + remote_gre_interface_ip_address
                        + "=>"
                        + remote_tunnel_end_ip
                        + "' >> "
                        + replace_file_path,
                    ]
                )
                return code1 == 0 and code2 == 0

            def add_lcp_interface(
                vpp_iface: str,
                host_iface: str,
                host_netns: str,
                is_tun: bool = False,
            ):
                iface_type = (
                    models.LCPHostInterfaceTypeEnum.TUN
                    if is_tun
                    else models.LCPHostInterfaceTypeEnum.TAP
                )
                reply = self.client.add_configuration(
                    models.LCPPairConfigurationItem(
                        interface=vpp_iface,
                        mirror_interface_host_name=host_iface,
                        mirror_interface_type=iface_type,
                        host_namespace=host_netns,
                    )
                )
                log.debug(reply)
                if reply.processing_error:
                    log.error(f"Error adding LCP interface: {reply.processing_error}")
                    return False
                return True

            def add_gre_tunnel_to_wg_and_lcp_it_to_frr_container(
                gre_interface_name: str,
                gre_interface_ip_address: str,
                gre_interface_ip_address_mask: str,
                remote_gre_interface_ip_address: str,
                interface_name: str,
                remote_tunnel_end_ip: str,
                frr_container_name: str = "frr",
            ):

                gre_iface = models.InterfaceConfigurationItem(
                    name=gre_interface_name,
                    type=models.InterfaceType.GRE_TUNNEL,
                    enabled=False,
                    ip_addresses=[],
                    link=models.GREInterfaceLink(
                        type=models.GRELinkType.L3,
                        src_addr=ipaddress.IPv4Address(gre_interface_ip_address),
                        dst_addr=ipaddress.IPv4Address(remote_gre_interface_ip_address),
                    ),
                )

                reply = self.client.add_configuration(gre_iface)
                if reply and reply.processing_error:
                    log.error(f"Error adding GRE interface: {reply.processing_error}")
                    return False

                self.add_vpp_route(
                    remote_gre_interface_ip_address
                    + "/32",  # route to one ip address => mask /32 is static value
                    remote_tunnel_end_ip,
                    interface_name,
                )

                add_lcp_interface(
                    gre_interface_name,
                    interface_name,
                    frr_container_name,
                    True,
                )

                reply = self.client.update_configuration(
                    gre_iface,
                    models.InterfaceConfigurationItem(
                        name=gre_interface_name,
                        type=models.InterfaceType.GRE_TUNNEL,
                        enabled=True,
                        ip_addresses=[
                            gre_interface_ip_address + "/" + gre_interface_ip_address_mask
                        ],
                        link=models.GREInterfaceLink(
                            type=models.GRELinkType.L3,
                            src_addr=ipaddress.IPv4Address(gre_interface_ip_address),
                            dst_addr=ipaddress.IPv4Address(remote_gre_interface_ip_address),
                        ),
                    ),
                )
                log.debug(reply)
                if reply and reply.processing_error:
                    log.error(f"Error enabling GRE interface: {reply.processing_error}")
                    return False
                return True

            if add_gre_tunnel_to_wg_and_lcp_it_to_frr_container(
                gre_interface_name,
                gre_interface_ip_address,
                gre_interface_ip_address_mask,
                remote_gre_interface_ip_address,
                tunnel.name,
                self._safe_extract_ip(tunnel.peer_ip_address) if hasattr(tunnel, 'peer_ip_address') and tunnel.peer_ip_address else remote_gre_interface_ip_address,
            ):
                log.info(f"Added GRE tunnel to VPP for {gre_interface_name}")

            run_command(
                [
                    "sudo",
                    "vppctl",
                    "ip",
                    "mroute",
                    "add",
                    remote_gre_interface_ip_address,
                    "224.0.0.5",  # static destination ip address of Hello OSPF packets, fixed by OSPF protocol itself
                    "via",
                    gre_interface_name,
                    "Accept",
                ]
            )
            run_command(
                [
                    "sudo",
                    "vppctl",
                    "ip",
                    "mroute",
                    "add",
                    remote_gre_interface_ip_address,
                    "224.0.0.5",  # static destination ip address of Hello OSPF packets, fixed by OSPF protocol itself
                    "via",
                    "local",  # static forwarding to local processing (it will be processed by LCP)
                    "Forward",
                ]
            )

            if add_frr_agent_ip_translation_from_gre_to_wg(
                gre_interface_ip_address,
                self._safe_extract_ip(tunnel.ip_address),
                remote_gre_interface_ip_address,
                self._safe_extract_ip(tunnel.peer_ip_address) if hasattr(tunnel, 'peer_ip_address') and tunnel.peer_ip_address else remote_gre_interface_ip_address,
            ):
                log.info(
                    f"Added FRR agent IP translation for GRE interface {gre_interface_name}"
                )
            
            return (
                gre_interface_name,
                local_gre_ip_with_mask,
            )
            
        except Exception as e:
            log.error(f"Failed to create GRE tunnel for {tunnel.name}: {e}")
            # Attempt cleanup on failure only if we used automatic generation
            if not (original_tunnel_config and 
                    original_tunnel_config.gre_name and 
                    original_tunnel_config.gre_local_tunnel_ip and 
                    original_tunnel_config.gre_remote_tunnel_ip):
                try:
                    gre_manager = get_gre_manager()
                    gre_manager.deallocate_gre_addresses(tunnel.name)
                except Exception as cleanup_error:
                    log.warning(f"Failed to cleanup GRE allocation for {tunnel.name}: {cleanup_error}")
            raise

    def expose_lan_interface_to_frr_container(
        self,
        interface: Interface,
        frr_container_name: str = "frr",
    ):
        lcp = f"sudo vppctl lcp create {interface.interface_name} host-if {interface.interface_name} netns {frr_container_name} tun"
        _, ret1 = run_command(lcp.split())
        if ret1 == 0:
            log.debug(f"Created LCP interface: {interface.interface_name}")
        else:
            log.error(f"Error creating LCP interface: {interface.interface_name}")
            return False

        ip_addr_del = f"sudo vppctl set interface ip address del {interface.interface_name} {str(interface.ip_address)}/{interface.prefix_len}"
        _, ret2 = run_command(ip_addr_del.split())

        if ret2 == 0:
            log.debug(f"Deleted IP address from interface: {interface.interface_name}")
        else:
            log.error(f"Error deleting IP address from interface: {interface.interface_name}")
            return False

        set_ip = f"sudo vppctl set interface ip address {interface.interface_name} {str(interface.ip_address)}/{interface.prefix_len}"
        _, ret3 = run_command(set_ip.split())

        if ret3 == 0:
            log.debug(f"Set IP address on interface: {interface.interface_name}")
        else:
            log.error(f"Error setting IP address on interface: {interface.interface_name}")
            return False

    def add_nat_configuration(
        self,
        lan_iface: Interface,
        wan_iface: Interface,
        wg_ports_to_open: List[int],
        tcp_ports: List[int] = [],
        udp_ports: List[int] = [],
    ):
        lan_nat_iface = models.Nat44InterfaceConfigurationItem(
            name=lan_iface.interface_name,
            nat_inside=True,
            nat_outside=False,
            output_feature=False,
        )
        wan_nat_iface = models.Nat44InterfaceConfigurationItem(
            name=wan_iface.interface_name,
            nat_inside=False,
            nat_outside=True,
            output_feature=False,
        )
        nat_address_pool = models.Nat44AddressPoolConfigurationItem(
            name="nat-pool",
            first_ip=ipaddress.IPv4Address(wan_iface.ip_address),
            last_ip=ipaddress.IPv4Address(wan_iface.ip_address),
        )

        wg_port_mappings = [
            models.IdentityMapping(
                interface=wan_iface.interface_name,
                protocol=models.ProtocolInNAT.UDP,
                port=p,
            )
            for p in wg_ports_to_open
        ]
        tcp_port_mappings = [
            models.IdentityMapping(
                interface=wan_iface.interface_name,
                protocol=models.ProtocolInNAT.TCP,
                port=p,
            )
            for p in tcp_ports
        ]
        udp_port_mappings = [
            models.IdentityMapping(
                interface=wan_iface.interface_name,
                protocol=models.ProtocolInNAT.UDP,
                port=p,
            )
            for p in udp_ports
        ]

        nat_mappings = models.DNat44ConfigurationItem(
            label="nat-mappings",
            static_mappings=[],
            identity_mappings=list(
                chain(wg_port_mappings, tcp_port_mappings, udp_port_mappings)
            ),
        )
        reply = self.client.add_configuration(
            lan_nat_iface,
            wan_nat_iface,
            nat_address_pool,
            nat_mappings,
        )
        log.debug(reply)
        if reply and reply.processing_error:
            log.error(f"Error adding NAT configuration: {reply.processing_error}")
            return False
        return True

    def add_acl_configuration(
        self,
        ingress_iface: Interface,
        deny_network: str,
    ):
        reply = self.client.add_configuration(
            models.ACLConfigurationItem(
                name="lab-acl-rules",
                ingress=[ingress_iface.interface_name],
                rules=[
                    models.ACLRuleConfigurationItem(
                        action=models.ACLAction.DENY,
                        refinement=models.IPSpecification(
                            addresses=models.IPAddresses(
                                destination_network=ipaddress.IPv4Network(deny_network)
                            )
                        ),
                    ),
                    models.ACLRuleConfigurationItem(
                        action=models.ACLAction.PERMIT,
                    ),
                ],
            )
        )
        log.debug(reply)
        if reply and reply.processing_error:
            log.error(f"Error adding ACL configuration: {reply.processing_error}")
            return False
        return True

    def delete_nat_configuration(
        self,
        lan_iface: Interface,
        wan_iface: Interface,
        wg_ports_to_open: List[int],
        tcp_ports: List[int] = [],
        udp_ports: List[int] = [],
    ):
        current_config = self.client.get_configuration()
        nat_config = [
            item.config
            for item in current_config.items
            if isinstance(item.config, models.Nat44InterfaceConfigurationItem)
            and item.config.name in [lan_iface.interface_name, wan_iface.interface_name]
        ]
        port_mappings = [
            item.config
            for item in current_config.items
            if isinstance(item.config, models.IdentityMapping)
            and item.config.port in chain(wg_ports_to_open, tcp_ports, udp_ports)
        ]

        if not nat_config:
            log.error("NAT configuration not found")
            return False
        reply = self.client.delete_configuration(*nat_config, *port_mappings)
        if reply.processing_error:
            log.error(f"Error deleting NAT configuration: {reply.processing_error}")
            return False
        log.info("NAT configuration deleted successfully")
        return True

    def delete_acl_configuration(self, lan_iface):
        current_config = self.client.get_configuration()
        acl_config = [
            item.config
            for item in current_config.items
            if isinstance(item.config, models.ACLConfigurationItem)
            and item.config.ingress == [lan_iface.interface_name]
        ]
        if not acl_config:
            log.error("ACL configuration not found")
            return False
        reply = self.client.delete_configuration(*acl_config)
        if reply.processing_error:
            log.error(f"Error deleting ACL configuration: {reply.processing_error}")
            return False
        log.info("ACL configuration deleted successfully")
        return True

    def remove_gre_tunnels(self) -> bool:
        """
        Remove GRE tunnels and deallocate their addresses.
        
        Returns:
            True if successful, False otherwise
        """
        try:
            gre_manager = get_gre_manager()
            success = True
            
            # Clean up GRE allocations for all WireGuard tunnels
            if self.wireguard_tunnels:
                for tunnel in self.wireguard_tunnels:
                    if not gre_manager.deallocate_gre_addresses(tunnel.name):
                        log.warning(f"Failed to deallocate GRE addresses for {tunnel.name}")
                        success = False
            
            # Clear the GRE tunnels list
            self.gre_tunnels = []
            
            if success:
                log.info("GRE tunnels removed and addresses deallocated successfully")
            else:
                log.warning("GRE tunnel removal completed with some warnings")
                
            return success
            
        except Exception as e:
            log.error(f"Exception while removing GRE tunnels: {e}")
            return False
    
    def _safe_extract_ip(self, ip_with_mask: str) -> str:
        """
        Safely extract IP address from IP/mask format.
        
        Args:
            ip_with_mask: IP address potentially with CIDR notation
            
        Returns:
            IP address without CIDR notation
        """
        if not ip_with_mask:
            return "0.0.0.0"  # Fallback
        
        if "/" in ip_with_mask:
            return ip_with_mask.split("/")[0]
        else:
            return ip_with_mask
