#!/usr/bin/env python3

# Check if we should be using the virtual environment's Python interpreter
import os
import sys
from pathlib import Path

def check_and_reexec_with_venv():
    """Check if we should re-execute with the virtual environment's Python"""
    # Skip if we're already running from venv or explicitly told not to use venv
    if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or os.environ.get('R1SETUP_NO_VENV'):
        return False
    
    # Get the real user's home directory (handles sudo scenarios)
    if 'SUDO_USER' in os.environ:
        real_user = os.environ['SUDO_USER']
        if os.name == 'posix':
            import pwd
            real_home = Path(pwd.getpwnam(real_user).pw_dir)
        else:
            real_home = Path.home()
    else:
        real_home = Path.home()
    
    # Check if virtual environment Python exists
    venv_python = real_home / '.ratio1' / 'r1_setup' / '.r1_venv' / 'bin' / 'python3'
    
    if venv_python.exists() and str(venv_python) != sys.executable:
        # Re-execute with virtual environment Python
        import subprocess
        try:
            os.execv(str(venv_python), [str(venv_python)] + sys.argv)
        except OSError:
            # If exec fails, continue with current Python
            pass
    
    return False

# Try to re-execute with virtual environment Python
check_and_reexec_with_venv()

import subprocess
import yaml
import getpass
import re
import json
import urllib.request
import urllib.error
import shutil
import tempfile
import stat
import ssl
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime

# Import version from ver.py
try:
    # Try to import from the same directory as this script
    script_dir = Path(__file__).parent
    sys.path.insert(0, str(script_dir))
    from ver import __VER__ as CLI_VERSION
except ImportError:
    # Fallback to hardcoded version if ver.py is not available
    CLI_VERSION = "1.1.6"

# Debug configuration
DEBUG = False  # Set to True to enable debug output, or use --debug flag

# Version information
UPDATE_CHECK_URL = "https://raw.githubusercontent.com/Ratio1/r1setup/refs/heads/main/mnl_factory/scripts/ver.py"
DOWNLOAD_BASE_URL = "https://github.com/Ratio1/multi_node_launcher/releases/download"

class R1Setup:
    def __init__(self):
        self.colors = {
            'red': '\033[91m',
            'green': '\033[92m',
            'yellow': '\033[93m',
            'blue': '\033[94m',
            'cyan': '\033[96m',
            'magenta': '\033[95m',
            'white': '\033[97m',
            'end': '\033[0m'
        }

        # Get the real user's home directory when running with sudo
        if 'SUDO_USER' in os.environ:
            import pwd
            real_user = os.environ['SUDO_USER']
            self.real_home = Path(pwd.getpwnam(real_user).pw_dir)
            self.real_user = real_user
        else:
            self.real_home = Path.home()
            self.real_user = os.environ.get('USER', 'unknown')

        # Detect OS
        self.os_type = self._detect_os()

        # Set up paths
        self.ratio1_base_dir = self.real_home / '.ratio1'
        self.r1_setup_dir = self.ratio1_base_dir / 'r1_setup'
        self.ansible_config_root = self.ratio1_base_dir / 'ansible_config'
        self.config_dir = self.ansible_config_root / 'collections/ansible_collections/ratio1/multi_node_launcher'

        # Configuration management paths
        self.configs_dir = self.r1_setup_dir / 'configs'
        self.active_config_file = self.r1_setup_dir / 'active_config.json'
        self.config_file = self.config_dir / 'hosts.yml'
        self.vars_file = self.config_dir / 'group_vars/variables.yml'

        # Create configs directory if it doesn't exist
        self.configs_dir.mkdir(parents=True, exist_ok=True)

        # Set installation directories based on OS
        if self.os_type == "macos":
            self.install_dir = self.real_home / "r1setup"
        else:
            self.install_dir = Path("/opt/r1setup")

        # Set up Ansible environment
        self._setup_ansible_env()

        # Initialize inventory
        self.inventory = {
            'all': {
                'vars': {},
                'children': {
                    'gpu_nodes': {
                        'hosts': {}
                    }
                }
            }
        }

        # Cache for version information to avoid duplicate checks
        self._version_cache = {
            'collection_version': None,
            'collection_check_time': None,
            'cache_duration': 300  # 5 minutes cache
        }

        # Load or initialize active configuration
        self._load_active_config()

    def _detect_os(self) -> str:
        """Detect the operating system"""
        os_name = os.uname().sysname
        if os_name == "Darwin":
            return "macos"
        elif os_name == "Linux":
            return "linux"
        else:
            self.print_colored(f"Unsupported OS: {os_name}", 'red')
            sys.exit(1)

    def _setup_ansible_env(self):
        """Set up Ansible environment variables"""
        os.environ['ANSIBLE_CONFIG'] = str(self.ansible_config_root / 'ansible.cfg')
        os.environ['ANSIBLE_COLLECTIONS_PATH'] = str(self.ansible_config_root / 'collections')
        os.environ['ANSIBLE_HOME'] = str(self.ansible_config_root)

    def _load_active_config(self) -> None:
        """Load the active configuration settings"""
        self.print_debug(f"Loading active config from: {self.active_config_file}")
        
        self.active_config = {
            'config_name': None,
            'environment': None,
            'created_at': None,
            'nodes_count': 0,
            'last_deployed_date': None,
            'last_deployed_network': None,
            'deployment_status': 'never_deployed',
            'last_deleted_date': None,
            'last_deployment_type': None
        }
        
        if self.active_config_file.exists():
            try:
                with open(self.active_config_file) as f:
                    loaded_config = json.load(f)
                    self.print_debug(f"Loaded active config from file: {loaded_config}")
                    self.active_config.update(loaded_config)
                    self.print_debug(f"Final active config after update: {self.active_config}")
                
                # Ensure network environment is synchronized after loading active config
                env = self.active_config.get('environment')
                if env:
                    self.set_mnl_app_env(env)
                    self.print_debug(f"Network environment synchronized after loading active config: {env}")
            except Exception as e:
                self.print_colored(f"Warning: Could not load active config: {e}", 'yellow')
                self.print_debug(f"Exception loading active config: {e}")
        else:
            self.print_debug(f"Active config file does not exist: {self.active_config_file}")

    def _save_active_config(self) -> None:
        """Save the active configuration settings"""
        try:
            self.print_debug(f"Saving active config to: {self.active_config_file}")
            self.print_debug(f"Active config being saved: {self.active_config}")
            with open(self.active_config_file, 'w') as f:
                json.dump(self.active_config, f, indent=2)
            self.print_debug(f"Successfully saved active config")
        except Exception as e:
            self.print_colored(f"Error saving active config: {e}", 'red')
            self.print_debug(f"Exception saving active config: {e}")

    def _generate_config_name(self, nodes_count: int, custom_name: str = None) -> str:
        """Generate a configuration name with user input, timestamp and metadata"""
        if not custom_name:
            self.print_colored("\n📝 Configuration Naming", 'cyan', bold=True)
            self.print_colored("Give your configuration a meaningful name to identify it later.", 'yellow')
            self.print_colored("Examples: 'production-cluster', 'test-env', 'gpu-farm-1'", 'white')
            
            while True:
                custom_name = self.get_input("Enter configuration name", required=True)
                # Validate name (allow letters, numbers, hyphens, underscores)
                if re.match(r'^[a-zA-Z0-9_-]+$', custom_name):
                    break
                self.print_colored("Invalid name. Use only letters, numbers, hyphens (-), and underscores (_)", 'red')
        
        # Generate timestamp components
        now = datetime.now()
        date_str = now.strftime('%Y%m%d')  # YYYYMMDD
        time_str = now.strftime('%H%M')    # HHMM
        
        # Create the final config name: customname_YYYYMMDD_HHMM_Nnodes
        config_name = f"{custom_name}_{date_str}_{time_str}_{nodes_count}n"
        
        return config_name

    def _list_available_configs(self) -> List[Tuple[str, Dict]]:
        """List all available configurations with their metadata"""
        configs = []
        # Look for all .yml files in configs directory (not just hosts_config_*)
        for config_file in self.configs_dir.glob("*.yml"):
            try:
                with open(config_file) as f:
                    config_data = yaml.safe_load(f)

                # Check if this is a valid configuration file by looking for the expected structure
                if not config_data or 'all' not in config_data:
                    continue
                if 'children' not in config_data.get('all', {}) or 'gpu_nodes' not in config_data.get('all', {}).get('children', {}):
                    continue

                # Extract metadata from config
                hosts = config_data.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
                metadata_file = config_file.with_suffix('.json')

                metadata = {
                    'nodes_count': len(hosts),
                    'environment': 'unknown',
                    'created_at': config_file.stat().st_mtime
                }

                if metadata_file.exists():
                    try:
                        with open(metadata_file) as f:
                            metadata.update(json.load(f))
                    except Exception:
                        pass

                configs.append((config_file.name, metadata))
            except Exception:
                continue

        # Sort by creation time, newest first
        configs.sort(key=lambda x: x[1]['created_at'], reverse=True)
        return configs

    def _save_config_with_metadata(self, config_name: str, environment: str, nodes_count: int, update_symlink: bool = True) -> None:
        """Save configuration with metadata"""
        config_path = self.configs_dir / f"{config_name}.yml"
        metadata_path = self.configs_dir / f"{config_name}.json"

        # Save the inventory configuration
        inventory_to_save = dict(self.inventory)
        # Remove environment from inventory as it's stored in metadata
        if 'vars' in inventory_to_save['all']:
            inventory_to_save['all']['vars'].pop('mnl_app_env', None)
            if not inventory_to_save['all']['vars']:
                inventory_to_save['all'].pop('vars', None)

        with open(config_path, 'w') as f:
            yaml.safe_dump(inventory_to_save, f, default_flow_style=False)

        os.chmod(config_path, 0o600)

        # Load existing metadata if it exists, otherwise create new
        if metadata_path.exists():
            try:
                with open(metadata_path) as f:
                    metadata = json.load(f)
                
                # Update specific fields
                metadata['environment'] = environment
                metadata['nodes_count'] = nodes_count
            except (json.JSONDecodeError, IOError):
                # If file is corrupted or unreadable, create a new one
                metadata = {}
        else:
            metadata = {}

        # Fill in any missing metadata fields
        if 'created_at' not in metadata:
            metadata['created_at'] = datetime.now().isoformat()
        if 'config_name' not in metadata:
            metadata['config_name'] = config_name
        if 'description' not in metadata:
            metadata['description'] = f"Configuration with {nodes_count} node(s) for {environment} network"
        if 'last_deployed_date' not in metadata:
            metadata['last_deployed_date'] = None
        if 'last_deployed_network' not in metadata:
            metadata['last_deployed_network'] = None
        if 'deployment_status' not in metadata:
            metadata['deployment_status'] = 'never_deployed'
        if 'last_deleted_date' not in metadata:
            metadata['last_deleted_date'] = None
        if 'last_deployment_type' not in metadata:
            metadata['last_deployment_type'] = None
        
        # Always update these fields
        metadata['environment'] = environment
        metadata['nodes_count'] = nodes_count


        with open(metadata_path, 'w') as f:
            json.dump(metadata, f, indent=2)

        # Update active config
        self.active_config.update(metadata)
        self._save_active_config()

        # Create/update symlink to active configuration only if requested
        if update_symlink:
            self._update_hosts_symlink(config_path)

    def _update_hosts_symlink(self, config_path: Path) -> None:
        """Update the hosts.yml symlink to point to the active configuration"""
        # Ensure the config directory exists
        self.config_dir.mkdir(parents=True, exist_ok=True)

        # Remove existing hosts.yml if it exists
        if self.config_file.exists() or self.config_file.is_symlink():
            self.config_file.unlink()

        # Create symlink to the active configuration
        try:
            self.config_file.symlink_to(config_path)
            self.print_colored(f"Active configuration linked to: {config_path.name}", 'green')
        except Exception as e:
            self.print_colored(f"Error creating symlink: {e}", 'red')

    def _load_config_by_name(self, config_name: str) -> bool:
        """Load a specific configuration by name"""
        config_path = self.configs_dir / f"{config_name}.yml"
        metadata_path = self.configs_dir / f"{config_name}.json"

        if not config_path.exists():
            return False

        try:
            # Load inventory
            with open(config_path) as f:
                self.inventory = yaml.safe_load(f) or self.inventory

            # Load metadata
            if metadata_path.exists():
                with open(metadata_path) as f:
                    metadata = json.load(f)
                self.active_config.update(metadata)
                self._save_active_config()

            # Update symlink
            self._update_hosts_symlink(config_path)

            # Set environment variable
            env = self.active_config.get('environment')
            if env:
                self.set_mnl_app_env(env)

            return True
        except Exception as e:
            self.print_colored(f"Error loading configuration: {e}", 'red')
            return False

    def print_colored(self, text: str, color: str = 'white', bold: bool = False, end: str = '\n') -> None:
        """Print colored text"""
        color_code = self.colors.get(color, self.colors['white'])
        if bold:
            color_code = '\033[1m' + color_code
        print(f"{color_code}{text}{self.colors['end']}", end=end)

    def print_debug(self, text: str, color: str = 'cyan') -> None:
        """Print debug text only when DEBUG is enabled"""
        if DEBUG:
            self.print_colored(f"[DEBUG] {text}", color)

    def print_header(self, title: str) -> None:
        """Print a formatted header"""
        self.print_colored("=" * 60, 'cyan')
        self.print_colored(f" {title.center(58)} ", 'cyan', bold=True)
        self.print_colored("=" * 60, 'cyan')

    def print_section(self, title: str) -> None:
        """Print a section header"""
        self.print_colored(f"\n{title}", 'yellow', bold=True)
        self.print_colored("-" * len(title), 'yellow')

    def get_input(self, prompt: str, default: str = '', required: bool = False) -> str:
        """Get user input with validation"""
        while True:
            default_str = f" [{default}]" if default else ""
            self.print_colored(f"{prompt}{default_str}: ", 'blue', end='')
            try:
                value = input().strip() or default
                if required and not value:
                    self.print_colored("This field cannot be empty. Please try again.", 'red')
                    continue
                return value
            except KeyboardInterrupt:
                self.print_colored("\nOperation cancelled by user.", 'yellow')
                sys.exit(0)

    def get_secure_input(self, prompt: str) -> str:
        """Get secure password input"""
        try:
            # Use getpass with the full prompt to avoid display issues
            return getpass.getpass(f"{prompt}: ")
        except (EOFError, KeyboardInterrupt):
            self.print_colored("\nOperation cancelled by user.", 'yellow')
            sys.exit(0)

    def validate_ip(self, ip: str) -> bool:
        """Validate IP address format"""
        pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
        if not re.match(pattern, ip):
            return False
        return all(0 <= int(part) <= 255 for part in ip.split('.'))

    def _validate_ssh_key_file(self, key_path: str) -> Dict[str, Any]:
        """Validate SSH private key file exists and is readable"""
        try:
            # Check if file exists
            if not os.path.exists(key_path):
                return {
                    'valid': False,
                    'error': f"File does not exist"
                }
            
            # Check if it's a file (not a directory)
            if not os.path.isfile(key_path):
                return {
                    'valid': False,
                    'error': f"Path is not a file"
                }
            
            # Check if file is readable
            if not os.access(key_path, os.R_OK):
                return {
                    'valid': False,
                    'error': f"File is not readable (check permissions)"
                }
            
            # Check file size (empty files are invalid)
            if os.path.getsize(key_path) == 0:
                return {
                    'valid': False,
                    'error': f"File is empty"
                }
            
            # Optional: Basic content validation for SSH key format
            try:
                with open(key_path, 'r') as f:
                    first_line = f.readline().strip()
                    # Check for common SSH private key headers
                    valid_headers = [
                        '-----BEGIN RSA PRIVATE KEY-----',
                        '-----BEGIN DSA PRIVATE KEY-----', 
                        '-----BEGIN EC PRIVATE KEY-----',
                        '-----BEGIN OPENSSH PRIVATE KEY-----',
                        '-----BEGIN PRIVATE KEY-----'
                    ]
                    
                    if not any(first_line.startswith(header) for header in valid_headers):
                        self.print_colored(f"Warning: File may not be a valid SSH private key format", 'yellow')
                        self.print_colored(f"Expected headers: RSA, DSA, EC, or OpenSSH format", 'yellow')
                        # Don't fail validation, just warn
            except (UnicodeDecodeError, IOError):
                # If we can't read as text, it might be a binary key format - that's okay
                pass
            
            return {
                'valid': True,
                'error': None
            }
            
        except Exception as e:
            return {
                'valid': False,
                'error': f"Unexpected error: {str(e)}"
            }

    def _get_valid_hostname(self, prompt: str, default: str = "") -> str:
        """Get a valid hostname with length and character restrictions"""
        self.print_colored("\n📝 Hostname Requirements:", 'cyan')
        self.print_colored("   • Maximum 15 characters", 'white')
        self.print_colored("   • Only letters (a-z, A-Z), numbers (0-9), hyphens (-), underscores (_)", 'white')
        self.print_colored("   • Cannot be empty", 'white')

        while True:
            hostname = self.get_input(prompt, default, required=True).strip()

            # Check if empty
            if not hostname:
                self.print_colored("Hostname cannot be empty. Please try again.", 'red')
                continue

            # Check character restrictions
            if not re.match(r'^[a-zA-Z0-9_-]+$', hostname):
                self.print_colored("Invalid characters in hostname.", 'red')
                self.print_colored("Only letters, numbers, hyphens (-), and underscores (_) are allowed.", 'red')
                continue

            # Check length
            if len(hostname) <= 15:
                return hostname

            # Hostname is too long, suggest shortened version
            shortened = hostname[:15]
            self.print_colored(f"Hostname '{hostname}' is too long ({len(hostname)} characters, max 15).", 'red')
            self.print_colored(f"Suggested shortened version: '{shortened}'", 'yellow')

            choice = self.get_input("Options:\n  1) Use shortened version\n  2) Enter a different name\nSelect option (1/2)", "1")

            if choice == '1':
                return shortened
            elif choice == '2':
                continue  # Ask for hostname again
            else:
                self.print_colored("Invalid choice. Please select 1 or 2.", 'red')
                continue

    def run_command(self, cmd: str, show_output: bool = True, shell: bool = True, timeout: int = None) -> tuple:
        """Run a shell command and return success status and output"""
        try:
            if show_output:
                self.print_colored(f"Running: {cmd}", 'cyan')

            if timeout and not show_output:
                # Use Popen for timeout cases where we want to capture partial output
                process = subprocess.Popen(
                    cmd,
                    shell=shell,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True
                )
                
                try:
                    stdout, _ = process.communicate(timeout=timeout)
                    return process.returncode == 0, stdout
                except subprocess.TimeoutExpired:
                    process.kill()
                    # Try to get any partial output
                    try:
                        stdout, _ = process.communicate(timeout=5)
                        self.print_colored(f"Command timed out after {timeout} seconds but captured partial output", 'yellow')
                        return False, stdout  # Return partial output even though it timed out
                    except subprocess.TimeoutExpired:
                        self.print_colored(f"Command timed out after {timeout} seconds: {cmd}", 'red')
                        return False, f"Command timed out after {timeout} seconds"
            else:
                # Use regular subprocess.run for other cases
                result = subprocess.run(
                    cmd,
                    shell=shell,
                    capture_output=not show_output,
                    text=True,
                    check=False,
                    timeout=timeout
                )

                if show_output:
                    return result.returncode == 0, ""
                else:
                    return result.returncode == 0, result.stdout
        except subprocess.TimeoutExpired:
            self.print_colored(f"Command timed out after {timeout} seconds: {cmd}", 'red')
            return False, f"Command timed out after {timeout} seconds"
        except Exception as e:
            self.print_colored(f"Error running command: {e}", 'red')
            return False, str(e)

    def check_ansible_installation(self) -> bool:
        """Check if Ansible is properly installed"""
        success, _ = self.run_command("ansible --version", show_output=False)
        if not success:
            self.print_colored("Ansible is not installed or not accessible!", 'red')
            return False

        # Check if collection is installed
        success, output = self.run_command(
            f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
            f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
            f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
            "ansible-galaxy collection list",
            show_output=False
        )

        if not success or "ratio1.multi_node_launcher" not in output:
            self.print_colored("Required Ansible collection is not installed!", 'red')
            return False

        return True

    def check_hosts_config(self) -> bool:
        """Check if hosts configuration exists and is valid"""
        if not self.config_file.exists():
            return False

        if self.config_file.stat().st_size == 0:
            return False

        try:
            with open(self.config_file) as f:
                config = yaml.safe_load(f)
                if not config or 'all' not in config:
                    return False
                hosts = config.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
                return len(hosts) > 0
        except Exception:
            return False

    def load_configuration(self) -> bool:
        """Load existing configuration"""
        if not self.config_file.exists():
            return False

        try:
            with open(self.config_file) as f:
                self.inventory = yaml.safe_load(f) or self.inventory
            
            # Ensure network environment is synchronized with active configuration
            env = self.active_config.get('environment')
            if env:
                self.set_mnl_app_env(env)
                self.print_debug(f"Network environment synchronized to: {env}")
            
            # Initialize status fields for existing nodes that don't have them
            hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
            updated = False
            self.print_debug(f"Checking status field initialization for {len(hosts)} nodes")
            
            for host_name, host_config in hosts.items():
                needs_update = False
                timestamp = datetime.now().isoformat()
                
                if 'node_status' not in host_config:
                    host_config['node_status'] = 'unknown'
                    needs_update = True
                
                if 'last_status_update' not in host_config:
                    host_config['last_status_update'] = timestamp
                    needs_update = True
                
                # Remove legacy last_status_check field if it exists
                if 'last_status_check' in host_config:
                    del host_config['last_status_check']
                    needs_update = True
                
                if needs_update:
                    updated = True
                    self.print_debug(f"Initialized missing status fields for node: {host_name}")
                    self.print_debug(f"  status={host_config.get('node_status')}, update={host_config.get('last_status_update')}")
                else:
                    current_status = host_config.get('node_status', 'unknown')
                    current_update = host_config.get('last_status_update', 'none')
                    self.print_debug(f"Node {host_name} has all status fields: status={current_status}, update={current_update}")
            
            # Save configuration if we updated any status fields
            if updated:
                self.print_debug(f"Saving configuration after initializing {sum(1 for host_config in hosts.values() if 'node_status' not in host_config)} status fields")
                self._save_configuration()
            else:
                self.print_debug("All nodes already have status fields, no updates needed")
            
            return True
        except Exception as e:
            self.print_colored(f"Error loading configuration: {e}", 'red')
            return False

    def get_mnl_app_env(self) -> Optional[str]:
        """Get the current network environment setting"""
        if self.vars_file.exists():
            try:
                with open(self.vars_file) as f:
                    data = yaml.safe_load(f) or {}
                    return data.get('mnl_app_env')
            except Exception:
                pass
        return self.inventory.get('all', {}).get('vars', {}).get('mnl_app_env')

    def set_mnl_app_env(self, env_value: str) -> None:
        """Set the network environment"""
        data = {}
        if self.vars_file.exists():
            try:
                with open(self.vars_file) as f:
                    data = yaml.safe_load(f) or {}
            except Exception:
                data = {}

        data['mnl_app_env'] = env_value
        self.vars_file.parent.mkdir(parents=True, exist_ok=True)

        with open(self.vars_file, 'w') as f:
            yaml.safe_dump(data, f, default_flow_style=False)

        self.inventory['all'].setdefault('vars', {})['mnl_app_env'] = env_value

    def show_main_menu(self) -> None:
        """Display the main menu"""
        # Reload active config to ensure deployment status is current
        self._load_active_config()
        
        self.print_header("Ratio1 Multi-Node Launcher Setup")

        # Show current status
        has_config = self.check_hosts_config()
        current_env = self.get_mnl_app_env()
        active_config_name = self.active_config.get('config_name', 'None')

        self.print_section("Current Status")
        
        self.print_colored(f"Active Config: {active_config_name if active_config_name != 'None' else '✗ No active config'}",
                           'green' if active_config_name != 'None' else 'red')
        self.print_colored(f"Configuration: {'✓ Configured' if has_config else '✗ Not configured'}",
                           'green' if has_config else 'red')
        self.print_colored(f"Network: {current_env if current_env else '✗ Not set'}",
                           'green' if current_env else 'red')
        
        # Show node status summary if configuration exists
        if has_config:
            self.load_configuration()
            hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
            if hosts:
                # Count nodes by status
                status_counts = {}
                for host_name in hosts.keys():
                    status_info = self._get_node_status_info(host_name)
                    status = status_info['status']
                    status_counts[status] = status_counts.get(status, 0) + 1
                
                # Display status summary
                status_summary = []
                for status, count in status_counts.items():
                    emoji, color, description = self._get_status_display_info(status)
                    status_summary.append(f"{emoji} {count} {description.lower()}")
                
                self.print_colored(f"Nodes ({len(hosts)}): {', '.join(status_summary)}", 'cyan')

        # Show deployment status
        deployment_status = self.active_config.get('deployment_status', 'never_deployed')
        last_deployed_date = self.active_config.get('last_deployed_date')
        last_deployed_network = self.active_config.get('last_deployed_network')
        last_deleted_date = self.active_config.get('last_deleted_date')

        if deployment_status == 'deployed' and last_deployed_date:
            try:
                deployed_dt = datetime.fromisoformat(last_deployed_date.replace('Z', '+00:00'))
                deployed_str = deployed_dt.strftime('%Y-%m-%d %H:%M')
                deployment_text = f"🚀 Last deployed: {deployed_str}"
                if last_deployed_network:
                    deployment_text += f" ({last_deployed_network})"
                self.print_colored(f"Deployment: {deployment_text}", 'green')
            except:
                self.print_colored("Deployment: ✓ Deployed", 'green')
        elif deployment_status == 'deleted' and last_deleted_date:
            try:
                deleted_dt = datetime.fromisoformat(last_deleted_date.replace('Z', '+00:00'))
                deleted_str = deleted_dt.strftime('%Y-%m-%d %H:%M')
                deployment_text = f"🗑️ Last deleted: {deleted_str}"
                self.print_colored(f"Deployment: {deployment_text}", 'red')
            except:
                self.print_colored("Deployment: ✓ Deleted", 'red')
        else:
            self.print_colored("Deployment: ✗ Never deployed", 'yellow')

        self.print_section("R1Setup Main Menu")
        print()
        self.print_colored("📋 CONFIGURATION", 'cyan', bold=True)
        self.print_colored("  1) Configuration Menu     - Node setup, environments, and management")
        print()
        self.print_colored("🚀 DEPLOYMENT", 'cyan', bold=True)
        self.print_colored("  2) Deployment Menu        - Deploy, delete, and manage deployments")
        print()
        self.print_colored("🔧 OPERATIONS", 'cyan', bold=True)
        self.print_colored("  3) Start Service          - Start the Edge Node service")
        self.print_colored("  4) Stop Service           - Stop the Edge Node service")
        self.print_colored("  5) Restart Service        - Restart the Edge Node service")
        print()
        self.print_colored("📊 MONITORING & INFO", 'cyan', bold=True)
        self.print_colored("  6) Node Status & Information - Get latest info and show detailed status")
        self.print_colored("  7) Get Node Addresses       - Display node blockchain addresses")
        self.print_colored("  8) Export to CSV            - Export node data to CSV file")
        self.print_colored("  9) Test Connectivity        - Verify connection to configured nodes")
        print()
        self.print_colored("🛠️ ADVANCED", 'cyan', bold=True)
        self.print_colored(" 10) Advanced Menu        - SSH, logs, and security tools")
        print()
        self.print_colored("  0) Exit")
        print()

    def configuration_menu(self) -> None:
        """Show configuration submenu"""
        while True:
            self.print_header("Configuration Menu")

            # Show current configuration status
            has_config = self.check_hosts_config()
            current_env = self.get_mnl_app_env()
            active_config_name = self.active_config.get('config_name', 'None')

            self.print_section("Current Status")
            self.print_colored(f"Active Config: {active_config_name if active_config_name != 'None' else '✗ No active config'}",
                               'green' if active_config_name != 'None' else 'red')
            self.print_colored(f"Configuration: {'✓ Configured' if has_config else '✗ Not configured'}",
                               'green' if has_config else 'red')
            self.print_colored(f"Network: {current_env if current_env else '✗ Not set'}",
                               'green' if current_env else 'red')

            # Show node count if configured
            if has_config:
                self.load_configuration()
                hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
                self.print_colored(f"Nodes: {len(hosts)} configured", 'cyan')

            self.print_colored("Configuration Menu", 'cyan', bold=True)
            print()
            self.print_colored("  1) Configure Nodes        - Node connection setup and management")
            self.print_colored("  2) Manage Configurations  - Switch, backup, restore configurations")
            self.print_colored("  3) View Configuration     - Display current configuration")
            self.print_colored("  4) Switch Environment     - Change network environment (mainnet/testnet/devnet)")
            print()
            self.print_colored("  0) Back to Main Menu")
            print()

            choice = self.get_input("Select option", "0")

            if choice == '0':
                break
            elif choice == '1':
                self.configure_nodes_menu()
            elif choice == '2':
                self.manage_configurations_menu()
            elif choice == '3':
                self.view_configuration()
            elif choice == '4':
                self.switch_environment()
            else:
                self.print_colored("Invalid option. Please try again.", 'red')
                input("Press Enter to continue...")

    def manage_configurations_menu(self) -> None:
        """Show configuration management submenu"""
        while True:
            self.print_header("Configuration Management")

            # List available configurations
            configs = self._list_available_configs()
            active_config_name = self.active_config.get('config_name')

            if configs:
                self.print_section(f"Available Configurations ({len(configs)})")
                for i, (config_name, metadata) in enumerate(configs, 1):
                    # Remove .yml extension and format display
                    display_name = config_name.replace('.yml', '')
                    env = metadata.get('environment', 'unknown')
                    nodes = metadata.get('nodes_count', 0)
                    created = metadata.get('created_at', '')
                    description = metadata.get('description', '')

                    # Extract custom name from the config name (everything before the first timestamp)
                    # Format: customname_YYYYMMDD_HHMM_Nnodes_environment
                    custom_name = display_name
                    if '_' in display_name:
                        parts = display_name.split('_')
                        if len(parts) >= 2:
                            # Find where the timestamp starts (8 digits)
                            for idx, part in enumerate(parts):
                                if len(part) == 8 and part.isdigit():
                                    custom_name = '_'.join(parts[:idx])
                                    break

                    # Format creation date
                    try:
                        if isinstance(created, str):
                            created_dt = datetime.fromisoformat(created.replace('Z', '+00:00'))
                            created_str = created_dt.strftime('%Y-%m-%d %H:%M')
                        else:
                            created_str = datetime.fromtimestamp(created).strftime('%Y-%m-%d %H:%M')
                    except:
                        created_str = "Unknown"

                    # Mark active configuration
                    active_marker = " ← ACTIVE" if display_name == active_config_name else ""

                    self.print_colored(f"  {i}. {custom_name}", 'cyan' if active_marker else 'white', bold=bool(active_marker))
                    info_line = f"     {env} | {nodes} node(s) | {created_str}{active_marker}"
                    if description:
                        info_line = f"     {description} | {created_str}{active_marker}"

                    # Add deployment status info
                    deployment_status = metadata.get('deployment_status', 'never_deployed')
                    last_deployed_date = metadata.get('last_deployed_date')
                    last_deployed_network = metadata.get('last_deployed_network')
                    last_deployment_type = metadata.get('last_deployment_type')
                    last_deleted_date = metadata.get('last_deleted_date')

                    if deployment_status == 'deployed' and last_deployed_date:
                        try:
                            deployed_dt = datetime.fromisoformat(last_deployed_date.replace('Z', '+00:00'))
                            deployed_str = deployed_dt.strftime('%Y-%m-%d %H:%M')
                            deployment_info = f"🚀 Last deployed: {deployed_str}"
                            if last_deployed_network:
                                deployment_info += f" ({last_deployed_network}"
                            if last_deployment_type:
                                deployment_info += f", {last_deployment_type})"
                            elif last_deployed_network:
                                deployment_info += ")"
                        except:
                            deployment_info = "🚀 Deployed"
                    elif deployment_status == 'deleted' and last_deleted_date:
                        try:
                            deleted_dt = datetime.fromisoformat(last_deleted_date.replace('Z', '+00:00'))
                            deleted_str = deleted_dt.strftime('%Y-%m-%d %H:%M')
                            deployment_info = f"🗑️ Last deleted: {deleted_str}"
                        except:
                            deployment_info = "🗑️ Deleted"
                    else:
                        deployment_info = "📋 Never deployed"

                    self.print_colored(info_line, 'green' if active_marker else 'white')
                    deployment_color = 'cyan' if deployment_status == 'deployed' else 'red' if deployment_status == 'deleted' else 'yellow'
                    self.print_colored(f"     {deployment_info}", deployment_color)
                print()

            self.print_colored("Manage Configurations Menu", 'cyan', bold=True)
            print()
            self.print_colored("  1) Create New Configuration       - Set up new configuration")
            if configs:
                self.print_colored("  2) Switch Configuration          - Activate different configuration")
                self.print_colored("  3) Delete Configuration          - Remove saved configuration")
                self.print_colored("  4) Rename Configuration          - Change configuration name")
                self.print_colored("")
                self.print_colored("  5) Backup Configuration          - Save configuration backup")
                self.print_colored("  6) Restore Configuration         - Restore from backup")
                self.print_colored("  7) Export Configuration          - Create portable config file")
                self.print_colored("  8) Quick Export Current Config   - Export active config")
            else:
                self.print_colored("  7) Export Configuration          - Create portable config file (no configs)")
                self.print_colored("  8) Quick Export Current          - Export active config")
            self.print_colored("  9) Import Configuration          - Import from portable file")
            print()
            self.print_colored("  0) Back to Main Menu")
            print()

            choice = self.get_input("Select option", "0")

            if choice == '0':
                break
            elif choice == '1':
                self._create_new_configuration_with_management()
            elif choice == '2' and configs:
                self._switch_configuration(configs)
            elif choice == '3' and configs:
                self._delete_configuration(configs)
            elif choice == '4' and configs:
                self._rename_configuration(configs)
            elif choice == '5' and configs:
                self._backup_configuration(configs)
            elif choice == '6' and configs:
                self._restore_configuration()
            elif choice == '7':
                if configs:
                    self._export_configuration(configs)
                else:
                    self.print_colored("No configurations available to export.", 'yellow')
                    input("Press Enter to continue...")
            elif choice == '8':
                self._quick_export_current()
            elif choice == '9':
                self._import_configuration()
            else:
                self.print_colored("Invalid option. Please try again.", 'red')
                input("Press Enter to continue...")

    def _create_new_configuration_with_management(self) -> None:
        """Create a new configuration with proper management"""
        self.print_section("Create New Configuration")

        # First step: Get configuration name
        self.print_colored("\n📝 Configuration Naming", 'cyan', bold=True)
        self.print_colored("Give your configuration a meaningful name to identify it later.", 'yellow')
        self.print_colored("Examples: 'production-cluster', 'test-env', 'gpu-farm-1'", 'white')

        while True:
            custom_name = self.get_input("Enter configuration name", required=True)
            # Validate name (allow letters, numbers, hyphens, underscores)
            if re.match(r'^[a-zA-Z0-9_-]+$', custom_name):
                break
            self.print_colored("Invalid name. Use only letters, numbers, hyphens (-), and underscores (_)", 'red')

        # Select network environment
        env = self._select_network_environment()

        # Get number of nodes
        while True:
            try:
                num_nodes = int(self.get_input("How many nodes do you want to configure", "1"))
                if num_nodes <= 0:
                    self.print_colored("Please enter a positive number", 'red')
                    continue
                break
            except ValueError:
                self.print_colored("Please enter a valid number", 'red')

        # Generate configuration name with the custom name
        config_name = self._generate_config_name(num_nodes, custom_name)

        # Reset inventory for new configuration
        self.inventory = {
            'all': {
                'vars': {},
                'children': {
                    'gpu_nodes': {
                        'hosts': {}
                    }
                }
            }
        }

        # Configure each node
        hosts = self.inventory['all']['children']['gpu_nodes']['hosts']
        for i in range(num_nodes):
            self.print_section(f"Configuring Node {i + 1} of {num_nodes}")
            name = self._get_valid_hostname(f"Enter name for node {i + 1}", f"gpu-node-{i + 1}")
            hosts[name] = self._configure_single_node()
            self.print_colored(f"Node '{name}' configured successfully!", 'green')

        # Save configuration with metadata
        self._save_config_with_metadata(config_name, env, num_nodes)
        self.print_colored(f"Configuration '{config_name}' created and activated!", 'green')
        input("Press Enter to continue...")

    def _switch_configuration(self, configs: List[Tuple[str, Dict]]) -> None:
        """Switch to a different configuration"""
        self.print_section("Switch Configuration")

        for i, (config_name, metadata) in enumerate(configs, 1):
            display_name = config_name.replace('.yml', '')
            env = metadata.get('environment', 'unknown')
            nodes = metadata.get('nodes_count', 0)
            self.print_colored(f"  {i}) {display_name} ({env}, {nodes} nodes)")

        while True:
            try:
                choice = int(self.get_input("Select configuration number", "1")) - 1
                if 0 <= choice < len(configs):
                    selected_config = configs[choice][0].replace('.yml', '')
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        if self._load_config_by_name(selected_config):
            self.print_colored(f"Switched to configuration: {selected_config}", 'green')
        else:
            self.print_colored("Failed to switch configuration", 'red')

        input("Press Enter to continue...")

    def _delete_configuration(self, configs: List[Tuple[str, Dict]]) -> None:
        """Delete a configuration"""
        self.print_section("Delete Configuration")

        for i, (config_name, metadata) in enumerate(configs, 1):
            display_name = config_name.replace('.yml', '')
            env = metadata.get('environment', 'unknown')
            nodes = metadata.get('nodes_count', 0)
            self.print_colored(f"  {i}) {display_name} ({env}, {nodes} nodes)")

        while True:
            try:
                choice = int(self.get_input("Select configuration number to delete", "1")) - 1
                if 0 <= choice < len(configs):
                    selected_config = configs[choice][0].replace('.yml', '')
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        if self.get_input(f"Delete configuration '{selected_config}'? (y/n)", "n").lower() == 'y':
            config_path = self.configs_dir / f"{selected_config}.yml"
            metadata_path = self.configs_dir / f"{selected_config}.json"

            try:
                # Delete files
                if config_path.exists():
                    config_path.unlink()
                if metadata_path.exists():
                    metadata_path.unlink()

                # If this was the active config, clear it
                if self.active_config.get('config_name') == selected_config:
                    self.active_config = {
                        'config_name': None,
                        'environment': None,
                        'created_at': None,
                        'nodes_count': 0,
                        'last_deployed_date': None,
                        'last_deployed_network': None,
                        'deployment_status': 'never_deployed',
                        'last_deleted_date': None,
                        'last_deployment_type': None
                    }
                    self._save_active_config()

                    # Remove symlink
                    if self.config_file.is_symlink():
                        self.config_file.unlink()

                self.print_colored(f"Configuration '{selected_config}' deleted successfully!", 'green')
            except Exception as e:
                self.print_colored(f"Error deleting configuration: {e}", 'red')

        input("Press Enter to continue...")

    def _rename_configuration(self, configs: List[Tuple[str, Dict]]) -> None:
        """Rename a configuration"""
        self.print_section("Rename Configuration")

        for i, (config_name, metadata) in enumerate(configs, 1):
            display_name = config_name.replace('.yml', '')
            env = metadata.get('environment', 'unknown')
            nodes = metadata.get('nodes_count', 0)
            self.print_colored(f"  {i}) {display_name} ({env}, {nodes} nodes)")

        while True:
            try:
                choice = int(self.get_input("Select configuration number to rename", "1")) - 1
                if 0 <= choice < len(configs):
                    old_config_name = configs[choice][0].replace('.yml', '')
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        new_name = self.get_input(f"Enter new name for '{old_config_name}'", required=True)

        # Validate new name
        if not re.match(r'^[a-zA-Z0-9_-]+$', new_name):
            self.print_colored("Invalid name. Use only letters, numbers, underscore, and hyphen.", 'red')
            input("Press Enter to continue...")
            return

        # Generate new config name with timestamp and metadata (like the original creation)
        # Extract environment and node count from existing metadata
        old_metadata_path = self.configs_dir / f"{old_config_name}.json"
        nodes_count = 1
        environment = 'mainnet'

        if old_metadata_path.exists():
            try:
                with open(old_metadata_path) as f:
                    metadata = json.load(f)
                nodes_count = metadata.get('nodes_count', 1)
                environment = metadata.get('environment', 'mainnet')
            except Exception:
                pass

        new_config_name = self._generate_config_name(nodes_count, new_name)

        # Check if new name already exists
        if (self.configs_dir / f"{new_config_name}.yml").exists():
            self.print_colored("A configuration with this name already exists!", 'red')
            input("Press Enter to continue...")
            return

        try:
            # Rename files
            old_config_path = self.configs_dir / f"{old_config_name}.yml"
            old_metadata_path = self.configs_dir / f"{old_config_name}.json"
            new_config_path = self.configs_dir / f"{new_config_name}.yml"
            new_metadata_path = self.configs_dir / f"{new_config_name}.json"

            old_config_path.rename(new_config_path)
            if old_metadata_path.exists():
                old_metadata_path.rename(new_metadata_path)

                # Update metadata
                with open(new_metadata_path) as f:
                    metadata = json.load(f)
                metadata['config_name'] = new_config_name

                with open(new_metadata_path, 'w') as f:
                    json.dump(metadata, f, indent=2)

            # Update active config if this was the active one
            if self.active_config.get('config_name') == old_config_name:
                self.active_config['config_name'] = new_config_name
                self._save_active_config()
                self._update_hosts_symlink(new_config_path)

            self.print_colored(f"Configuration renamed from '{old_config_name}' to '{new_config_name}'!", 'green')
        except Exception as e:
            self.print_colored(f"Error renaming configuration: {e}", 'red')

        input("Press Enter to continue...")

    def _backup_configuration(self, configs: List[Tuple[str, Dict]]) -> None:
        """Create a backup of a configuration"""
        self.print_section("Backup Configuration")

        for i, (config_name, metadata) in enumerate(configs, 1):
            display_name = config_name.replace('.yml', '')
            env = metadata.get('environment', 'unknown')
            nodes = metadata.get('nodes_count', 0)
            self.print_colored(f"  {i}) {display_name} ({env}, {nodes} nodes)")

        while True:
            try:
                choice = int(self.get_input("Select configuration number to backup", "1")) - 1
                if 0 <= choice < len(configs):
                    config_name = configs[choice][0].replace('.yml', '')
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        try:
            # Create backup directory if it doesn't exist
            backup_dir = self.configs_dir / 'backups'
            backup_dir.mkdir(parents=True, exist_ok=True)

            # Generate backup filename with timestamp
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            backup_name = f"{config_name}_backup_{timestamp}"
            
            # Copy configuration files
            config_path = self.configs_dir / f"{config_name}.yml"
            metadata_path = self.configs_dir / f"{config_name}.json"
            
            backup_config_path = backup_dir / f"{backup_name}.yml"
            backup_metadata_path = backup_dir / f"{backup_name}.json"
            
            if config_path.exists():
                shutil.copy2(config_path, backup_config_path)
            if metadata_path.exists():
                shutil.copy2(metadata_path, backup_metadata_path)
            
            self.print_colored(f"✅ Configuration '{config_name}' backed up as '{backup_name}'", 'green')
            self.print_colored(f"📁 Backup location: {backup_dir}", 'cyan')
            
        except Exception as e:
            self.print_colored(f"❌ Error creating backup: {e}", 'red')

        input("Press Enter to continue...")

    def _restore_configuration(self) -> None:
        """Restore a configuration from backup"""
        self.print_section("Restore Configuration")

        # Check if backup directory exists
        backup_dir = self.configs_dir / 'backups'
        if not backup_dir.exists():
            self.print_colored("No backups found. Backup directory doesn't exist.", 'yellow')
            input("Press Enter to continue...")
            return

        # Get list of backup files
        backup_files = list(backup_dir.glob('*_backup_*.yml'))
        if not backup_files:
            self.print_colored("No backup files found.", 'yellow')
            input("Press Enter to continue...")
            return

        # Parse backup information
        backups = []
        for backup_file in backup_files:
            backup_name = backup_file.stem
            # Extract original name and timestamp from backup name
            # Format: originalname_backup_YYYYMMDD_HHMMSS
            parts = backup_name.split('_backup_')
            if len(parts) == 2:
                original_name = parts[0]
                timestamp_str = parts[1]
                try:
                    timestamp = datetime.strptime(timestamp_str, '%Y%m%d_%H%M%S')
                    backups.append((backup_name, original_name, timestamp))
                except ValueError:
                    continue

        if not backups:
            self.print_colored("No valid backup files found.", 'yellow')
            input("Press Enter to continue...")
            return

        # Sort backups by timestamp (newest first)
        backups.sort(key=lambda x: x[2], reverse=True)

        # Show available backups
        self.print_colored("Available Backups:")
        for i, (backup_name, original_name, timestamp) in enumerate(backups, 1):
            timestamp_str = timestamp.strftime('%Y-%m-%d %H:%M:%S')
            self.print_colored(f"  {i}) {original_name} (backed up on {timestamp_str})")

        while True:
            try:
                choice = int(self.get_input("Select backup number to restore", "1")) - 1
                if 0 <= choice < len(backups):
                    selected_backup = backups[choice]
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        backup_name, original_name, timestamp = selected_backup
        
        # Get new name for restored configuration
        self.print_colored(f"\nRestoring backup of '{original_name}' from {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
        new_name = self.get_input(f"Enter name for restored configuration (default: {original_name}_restored)", 
                                  f"{original_name}_restored")

        # Validate new name
        if not re.match(r'^[a-zA-Z0-9_-]+$', new_name):
            self.print_colored("Invalid name. Use only letters, numbers, underscore, and hyphen.", 'red')
            input("Press Enter to continue...")
            return

        try:
            # Load backup metadata to get proper configuration structure
            backup_metadata_path = backup_dir / f"{backup_name}.json"
            nodes_count = 1
            environment = 'mainnet'
            
            if backup_metadata_path.exists():
                try:
                    with open(backup_metadata_path) as f:
                        metadata = json.load(f)
                    nodes_count = metadata.get('nodes_count', 1)
                    environment = metadata.get('environment', 'mainnet')
                except Exception:
                    pass

            # Generate proper configuration name
            restored_config_name = self._generate_config_name(nodes_count, new_name)

            # Check if restored name already exists
            if (self.configs_dir / f"{restored_config_name}.yml").exists():
                self.print_colored("A configuration with this name already exists!", 'red')
                input("Press Enter to continue...")
                return

            # Copy backup files to main config directory
            backup_config_path = backup_dir / f"{backup_name}.yml"
            backup_metadata_path = backup_dir / f"{backup_name}.json"
            
            restored_config_path = self.configs_dir / f"{restored_config_name}.yml"
            restored_metadata_path = self.configs_dir / f"{restored_config_name}.json"

            if backup_config_path.exists():
                shutil.copy2(backup_config_path, restored_config_path)
            if backup_metadata_path.exists():
                shutil.copy2(backup_metadata_path, restored_metadata_path)
                
                # Update metadata with new name
                with open(restored_metadata_path) as f:
                    metadata = json.load(f)
                metadata['config_name'] = restored_config_name
                metadata['restored_from'] = backup_name
                metadata['restored_at'] = datetime.now().isoformat()
                
                with open(restored_metadata_path, 'w') as f:
                    json.dump(metadata, f, indent=2)

            self.print_colored(f"✅ Configuration restored as '{restored_config_name}'", 'green')
            self.print_colored("💡 Use 'Switch Configuration' to activate the restored configuration.", 'cyan')
            
        except Exception as e:
            self.print_colored(f"❌ Error restoring configuration: {e}", 'red')

        input("Press Enter to continue...")

    def _export_configuration(self, configs: List[Tuple[str, Dict]]) -> None:
        """Export configuration to a portable file for transfer between machines"""
        self.print_section("Export Configuration")
        
        if not configs:
            self.print_colored("No configurations available to export.", 'yellow')
            input("Press Enter to continue...")
            return

        # Show available configurations
        self.print_colored("Available configurations to export:")
        for i, (config_name, metadata) in enumerate(configs, 1):
            display_name = config_name.replace('.yml', '')
            env = metadata.get('environment', 'unknown')
            nodes = metadata.get('nodes_count', 0)
            created = metadata.get('created_at', 'unknown')
            if isinstance(created, str) and 'T' in created:
                try:
                    created = datetime.fromisoformat(created.replace('Z', '+00:00')).strftime('%Y-%m-%d %H:%M')
                except:
                    pass
            self.print_colored(f"  {i}) {display_name} ({env}, {nodes} nodes, created: {created})")

        # Select configuration to export
        while True:
            try:
                choice = int(self.get_input("Select configuration number to export", "1")) - 1
                if 0 <= choice < len(configs):
                    config_name = configs[choice][0].replace('.yml', '')
                    config_metadata = configs[choice][1]
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        # Get export destination
        default_filename = f"{config_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.r1config"
        filename = self.get_input(f"Export filename [{default_filename}]", default_filename)
        
        # Ensure .r1config extension
        if not filename.endswith('.r1config'):
            filename += '.r1config'
        
        export_path = Path(filename)
        if not export_path.is_absolute():
            export_path = Path.cwd() / filename

        try:
            # Load configuration files
            config_path = self.configs_dir / f"{config_name}.yml"
            metadata_path = self.configs_dir / f"{config_name}.json"
            
            # Read configuration data
            with open(config_path) as f:
                config_data = yaml.safe_load(f)
            
            with open(metadata_path) as f:
                metadata = json.load(f)

            # Read current environment variables
            env_data = {}
            if self.vars_file.exists():
                with open(self.vars_file) as f:
                    env_data = yaml.safe_load(f) or {}

            # Create unified export structure
            export_data = {
                'format_version': '1.0',
                'exported_at': datetime.now().isoformat(),
                'exported_by': self.real_user,
                'export_source': 'r1setup',
                'configuration': {
                    'name': config_name,
                    'metadata': metadata,
                    'inventory': config_data,
                    'environment_vars': env_data,
                    'ansible_vars': {}
                }
            }

            # Include relevant ansible variables if they exist
            ansible_vars_files = [
                self.config_dir / 'group_vars/all.yml',
                self.config_dir / 'group_vars/mnl.yml'
            ]
            
            for var_file in ansible_vars_files:
                if var_file.exists():
                    with open(var_file) as f:
                        var_data = yaml.safe_load(f) or {}
                        export_data['configuration']['ansible_vars'][var_file.name] = var_data

            # Write export file
            with open(export_path, 'w') as f:
                json.dump(export_data, f, indent=2, default=str)

            self.print_colored(f"✅ Configuration '{config_name}' exported successfully!", 'green')
            self.print_colored(f"📁 Export file: {export_path}", 'cyan')
            self.print_colored(f"📝 File size: {export_path.stat().st_size} bytes", 'white')
            
            # Show what was exported
            hosts_count = len(config_data.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {}))
            self.print_colored(f"🔧 Exported: {hosts_count} nodes, {metadata.get('environment', 'unknown')} environment", 'cyan')
            
            self.print_colored("\n💡 To import on another machine:", 'yellow')
            self.print_colored(f"   1. Copy {filename} to the target machine", 'white')
            self.print_colored(f"   2. Run 'r1setup' and select Configuration Management → Import Configuration", 'white')
            
        except Exception as e:
            self.print_colored(f"❌ Error exporting configuration: {e}", 'red')

        input("\nPress Enter to continue...")

    def _import_configuration(self) -> None:
        """Import configuration from a portable file"""
        self.print_section("Import Configuration")
        
        self.print_colored("This will import a configuration from an .r1config file.", 'cyan')
        self.print_colored("The file should have been created using the Export Configuration option.", 'yellow')
        
        # Get import file path
        import_file = self.get_input("Enter path to .r1config file")
        import_path = Path(import_file)
        
        if not import_path.is_absolute():
            import_path = Path.cwd() / import_file

        if not import_path.exists():
            self.print_colored(f"❌ File not found: {import_path}", 'red')
            input("Press Enter to continue...")
            return

        try:
            # Read and validate import file
            with open(import_path) as f:
                import_data = json.load(f)

            # Validate format
            if not isinstance(import_data, dict) or 'configuration' not in import_data:
                self.print_colored("❌ Invalid configuration file format", 'red')
                input("Press Enter to continue...")
                return

            config_info = import_data['configuration']
            metadata = config_info.get('metadata', {})
            inventory = config_info.get('inventory', {})
            env_vars = config_info.get('environment_vars', {})
            ansible_vars = config_info.get('ansible_vars', {})

            # Show import details
            original_name = config_info.get('name', 'unknown')
            nodes_count = len(inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {}))
            environment = metadata.get('environment', 'unknown')
            exported_at = import_data.get('exported_at', 'unknown')
            exported_by = import_data.get('exported_by', 'unknown')

            self.print_colored(f"\n📋 Configuration Details:", 'cyan', bold=True)
            self.print_colored(f"   Original name: {original_name}")
            self.print_colored(f"   Environment: {environment}")
            self.print_colored(f"   Nodes: {nodes_count}")
            self.print_colored(f"   Exported: {exported_at}")
            self.print_colored(f"   Exported by: {exported_by}")

            # Show nodes that will be imported
            hosts = inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
            if hosts:
                self.print_colored(f"\n🖥️  Nodes to import:", 'yellow', bold=True)
                for hostname, config in hosts.items():
                    ip = config.get('ansible_host', 'unknown')
                    user = config.get('ansible_user', 'unknown')
                    self.print_colored(f"   • {hostname} ({ip}) - user: {user}")

            # Confirm import
            if self.get_input(f"\nProceed with import? (y/n)", "y").lower() != 'y':
                self.print_colored("Import cancelled.", 'yellow')
                input("Press Enter to continue...")
                return

            # Get new configuration name
            suggested_name = self.get_input(f"Configuration name [{original_name}]", original_name)
            
            # Generate proper config filename
            final_config_name = self._generate_config_name(nodes_count, suggested_name)
            
            # Check if config already exists
            if (self.configs_dir / f"{final_config_name}.yml").exists():
                if self.get_input(f"Configuration '{final_config_name}' already exists. Overwrite? (y/n)", "n").lower() != 'y':
                    self.print_colored("Import cancelled.", 'yellow')
                    input("Press Enter to continue...")
                    return

            # Save imported configuration
            config_path = self.configs_dir / f"{final_config_name}.yml"
            metadata_path = self.configs_dir / f"{final_config_name}.json"

            # Update metadata
            updated_metadata = dict(metadata)
            updated_metadata['config_name'] = final_config_name
            updated_metadata['imported_at'] = datetime.now().isoformat()
            updated_metadata['imported_from'] = str(import_path)
            updated_metadata['original_name'] = original_name

            # Save configuration files
            with open(config_path, 'w') as f:
                yaml.safe_dump(inventory, f, default_flow_style=False)
            
            with open(metadata_path, 'w') as f:
                json.dump(updated_metadata, f, indent=2)

            os.chmod(config_path, 0o600)

            # Update environment variables if included
            if env_vars:
                current_env_vars = {}
                if self.vars_file.exists():
                    try:
                        with open(self.vars_file) as f:
                            current_env_vars = yaml.safe_load(f) or {}
                    except:
                        pass
                
                # Merge environment variables (imported ones take precedence)
                current_env_vars.update(env_vars)
                
                # Ensure directory exists
                self.vars_file.parent.mkdir(parents=True, exist_ok=True)
                
                with open(self.vars_file, 'w') as f:
                    yaml.safe_dump(current_env_vars, f, default_flow_style=False)

            # Apply ansible variables if included
            for var_filename, var_content in ansible_vars.items():
                var_file_path = self.config_dir / 'group_vars' / var_filename
                var_file_path.parent.mkdir(parents=True, exist_ok=True)
                
                # Only update if the imported file has content
                if var_content:
                    with open(var_file_path, 'w') as f:
                        yaml.safe_dump(var_content, f, default_flow_style=False)

            self.print_colored(f"✅ Configuration imported successfully as '{final_config_name}'!", 'green')
            self.print_colored(f"📁 Saved to: {config_path}", 'cyan')
            
            # Ask if user wants to activate the imported configuration
            if self.get_input("Activate this configuration now? (y/n)", "y").lower() == 'y':
                self.inventory = inventory
                self.active_config.update(updated_metadata)
                self._save_active_config()
                self._update_hosts_symlink(config_path)
                
                # Set environment
                env = updated_metadata.get('environment')
                if env:
                    self.set_mnl_app_env(env)
                
                self.print_colored("✅ Configuration activated!", 'green')
            else:
                self.print_colored("💡 Use 'Switch Configuration' to activate it later.", 'cyan')

        except json.JSONDecodeError:
            self.print_colored("❌ Invalid JSON in configuration file", 'red')
        except yaml.YAMLError as e:
            self.print_colored(f"❌ Invalid YAML in configuration: {e}", 'red')
        except Exception as e:
            self.print_colored(f"❌ Error importing configuration: {e}", 'red')

        input("\nPress Enter to continue...")

    def _quick_export_current(self) -> None:
        """Quick export of current active configuration"""
        if not self.check_hosts_config():
            self.print_colored("No active configuration to export.", 'yellow')
            input("Press Enter to continue...")
            return

        # Generate filename based on active config
        config_name = self.active_config.get('config_name', 'current_config')
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f"{config_name}_{timestamp}.r1config"
        
        try:
            # Create export in current directory
            export_path = Path.cwd() / filename

            # Read current configuration
            with open(self.config_file) as f:
                config_data = yaml.safe_load(f)

            # Read environment variables
            env_data = {}
            if self.vars_file.exists():
                with open(self.vars_file) as f:
                    env_data = yaml.safe_load(f) or {}

            # Create export structure
            export_data = {
                'format_version': '1.0',
                'exported_at': datetime.now().isoformat(),
                'exported_by': self.real_user,
                'export_source': 'r1setup_quick',
                'configuration': {
                    'name': config_name,
                    'metadata': dict(self.active_config),
                    'inventory': config_data,
                    'environment_vars': env_data,
                    'ansible_vars': {}
                }
            }

            # Include ansible variables
            ansible_vars_files = [
                self.config_dir / 'group_vars/all.yml',
                self.config_dir / 'group_vars/mnl.yml'
            ]
            
            for var_file in ansible_vars_files:
                if var_file.exists():
                    with open(var_file) as f:
                        var_data = yaml.safe_load(f) or {}
                        export_data['configuration']['ansible_vars'][var_file.name] = var_data

            # Write export file
            with open(export_path, 'w') as f:
                json.dump(export_data, f, indent=2, default=str)

            self.print_colored(f"✅ Current configuration exported to: {filename}", 'green')
            nodes = len(config_data.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {}))
            env = self.active_config.get('environment', 'unknown')
            self.print_colored(f"🔧 Exported: {nodes} nodes, {env} environment", 'cyan')

        except Exception as e:
            self.print_colored(f"❌ Error exporting configuration: {e}", 'red')

        input("\nPress Enter to continue...")

    def configure_nodes_menu(self) -> None:
        """Show node configuration submenu"""
        while True:
            self.print_header("Node Configuration")

            # Load current configuration
            self.load_configuration()
            hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})

            if hosts:
                self.print_section(f"Current Nodes ({len(hosts)})")
                for i, (name, config) in enumerate(hosts.items(), 1):
                    ip = config.get('ansible_host', 'Unknown')
                    user = config.get('ansible_user', 'Unknown')
                    
                    # Get status information
                    status_info = self._get_node_status_info(name)
                    status = status_info['status']
                    status_emoji, status_color, status_desc = self._get_status_display_info(status)
                    
                    self.print_colored(f"  {i}. {name} ({user}@{ip}) ", 'white', end='')
                    self.print_colored(f"[{status_emoji} {status_desc}]", status_color)
                print()

            self.print_colored("Configure Nodes Menu", 'cyan', bold=True)
            print()
            if not hosts:
                self.print_colored("  1) Create Initial Configuration    - First-time setup")
            else:
                self.print_colored("  1) Add New Node                   - Add node to existing config")
                self.print_colored("  2) Edit Existing Node             - Modify node settings")
                self.print_colored("  3) Remove Node                    - Delete node from config")
                self.print_colored("  4) Create New Configuration       - Start fresh configuration")
            print()
            self.print_colored("  0) Back to Main Menu")
            print()

            choice = self.get_input("Select option", "0")

            if choice == '0':
                break
            elif choice == '1':
                if not hosts:
                    self._create_initial_configuration()
                else:
                    self._add_node()
            elif choice == '2' and hosts:
                self._update_node()
            elif choice == '3' and hosts:
                self._delete_node()
            elif choice == '4' and hosts:
                self._create_new_configuration()
            else:
                self.print_colored("Invalid option. Please try again.", 'red')
                input("Press Enter to continue...")

    def deployment_menu(self) -> None:
        """Show deployment submenu"""
        while True:
            # Reload active config to ensure deployment status is current
            self._load_active_config()
            
            self.print_header("Deployment Menu")

            # Show deployment status overview
            deployment_status = self.active_config.get('deployment_status', 'never_deployed')
            last_deployed_date = self.active_config.get('last_deployed_date')
            last_deployed_network = self.active_config.get('last_deployed_network')
            last_deployment_type = self.active_config.get('last_deployment_type')
            last_deleted_date = self.active_config.get('last_deleted_date')

            # Show current deployment status
            self.print_section("Current Deployment Status")
            if deployment_status == 'deployed' and last_deployed_date:
                try:
                    deployed_dt = datetime.fromisoformat(last_deployed_date.replace('Z', '+00:00'))
                    deployed_str = deployed_dt.strftime('%Y-%m-%d %H:%M')
                    self.print_colored(f"🚀 Status: Deployed on {deployed_str}", 'green')
                    if last_deployed_network:
                        self.print_colored(f"🌐 Network: {last_deployed_network}", 'cyan')
                    if last_deployment_type:
                        self.print_colored(f"🔧 Type: {last_deployment_type}", 'cyan')
                except:
                    self.print_colored("🚀 Status: Deployed", 'green')
            elif deployment_status == 'deleted' and last_deleted_date:
                try:
                    deleted_dt = datetime.fromisoformat(last_deleted_date.replace('Z', '+00:00'))
                    deleted_str = deleted_dt.strftime('%Y-%m-%d %H:%M')
                    self.print_colored(f"🗑️ Status: Deleted on {deleted_str}", 'red')
                except:
                    self.print_colored("🗑️ Status: Deleted", 'red')
            else:
                self.print_colored("📋 Status: Never deployed", 'yellow')

            self.print_colored("Deployment Menu", 'cyan', bold=True)
            print()
            self.print_colored("  1) Deploy with GPU        - Deploy full Edge Node with GPU support")
            self.print_colored("  2) Deploy CPU Only        - Deploy Edge Node without GPU capabilities")
            self.print_colored("  3) Delete Deployment      - Remove deployed Edge Node")
            self.print_colored("  4) Deployment Status      - Check detailed deployment status")
            print()
            self.print_colored("  0) Back to Main Menu")
            print()

            choice = self.get_input("Select option", "0")

            if choice == '0':
                break
            elif choice == '1':
                self.deploy_full()
            elif choice == '2':
                self.deploy_docker_only()
            elif choice == '3':
                self.delete_edge_node()
            elif choice == '4':
                self.deployment_status()
            else:
                self.print_colored("Invalid option. Please try again.", 'red')
                input("Press Enter to continue...")

    def advanced_menu(self) -> None:
        """Show advanced menu with utilities and expert tools"""
        while True:
            self.print_header("Advanced Menu")

            # Show if configuration exists
            has_config = self.check_hosts_config()
            if has_config:
                self.load_configuration()
                hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
                self.print_section(f"Available for {len(hosts)} configured node(s)")
            else:
                self.print_section("No nodes configured")
                self.print_colored("⚠️  Configure nodes first to use advanced tools", 'yellow')

            self.print_colored("Advanced Menu", 'cyan', bold=True)
            print()
            self.print_colored("🔧 OPERATIONAL TOOLS", 'cyan', bold=True)
            self.print_colored("  1) SSH to Node           - Connect to node via SSH")
            self.print_colored("  2) Get Logs              - Stream logs from nodes")
            self.print_colored("  3) Write Logs to File    - Save node logs to local file")
            print()
            self.print_colored("🔐 SECURITY TOOLS", 'red', bold=True)
            self.print_colored("  4) Import Private Keys   - Collect private keys from all nodes")
            print()
            self.print_colored("  0) Back to Main Menu")
            print()

            choice = self.get_input("Select option", "0")

            if choice == '0':
                break
            elif choice == '1':
                self.ssh_into_node_machine()
            elif choice == '2':
                self.get_logs()
            elif choice == '3':
                self.write_logs_to_file()
            elif choice == '4':
                self.import_nodes_private_keys()
            else:
                self.print_colored("Invalid option. Please try again.", 'red')
                input("Press Enter to continue...")



    def import_nodes_private_keys(self) -> None:
        """Import private keys from all configured nodes"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Import Nodes Private Keys")
        
        # WARNING MESSAGE
        self.print_section("⚠️  CRITICAL SECURITY WARNING")
        self.print_colored("This operation will collect private keys from all configured nodes.", 'red', bold=True)
        self.print_colored("Private keys provide full access to node wallets and should be handled with extreme care.", 'red')
        self.print_colored("Only proceed if you understand the security implications.", 'red')
        print()
        
        # Load configuration
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        
        if not hosts:
            self.print_colored("No nodes configured.", 'yellow')
            input("Press Enter to continue...")
            return
        
        # Show nodes that will be processed
        self.print_section(f"Target Nodes ({len(hosts)})")
        for host_name, config in hosts.items():
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            self.print_colored(f"  • {host_name}: {user}@{ip}", 'white')
        
        # Confirm operation
        print()
        self.print_colored("This will:", 'yellow', bold=True)
        self.print_colored("  1. Connect to each node via SSH", 'yellow')
        self.print_colored("  2. Retrieve the private key file: /var/cache/edge_node/_local_cache/_data/e2.pem", 'yellow')
        self.print_colored("  3. Create a local 'node_keys' folder", 'yellow')
        self.print_colored("  4. Save each key as: node_keys/{node_name}_e2.pem", 'yellow')
        print()
        
        # Final confirmation
        confirm = self.get_input("Are you sure you want to proceed? (yes/no)", "no")
        if confirm.lower() not in ['yes', 'y']:
            self.print_colored("Operation cancelled.", 'yellow')
            input("Press Enter to continue...")
            return
        
        # Create keys directory
        keys_dir = os.path.join(os.getcwd(), 'node_keys')
        try:
            os.makedirs(keys_dir, exist_ok=True)
            self.print_colored(f"Created keys directory: {keys_dir}", 'green')
        except Exception as e:
            self.print_colored(f"Failed to create keys directory: {e}", 'red')
            input("Press Enter to continue...")
            return
        
        # Process each node
        self.print_section("Collecting Private Keys")
        successful_imports = 0
        failed_imports = 0
        
        for host_name, config in hosts.items():
            self.print_colored(f"\n📡 Processing {host_name}...", 'cyan')
            
            # Get connection details
            ip = config.get('ansible_host')
            user = config.get('ansible_user')
            
            if not ip or not user:
                self.print_colored(f"❌ Missing connection details for {host_name}", 'red')
                failed_imports += 1
                continue
            
            # Build SSH command to retrieve the private key
            ssh_cmd = ['ssh']
            
            # Add port if specified
            ssh_port = config.get('ansible_port', 22)
            if ssh_port != 22:
                ssh_cmd.extend(['-p', str(ssh_port)])
            
            # Handle authentication
            if 'ansible_ssh_pass' in config:
                # For password authentication, we need to use sshpass
                ssh_cmd = ['sshpass', '-p', config['ansible_ssh_pass']] + ssh_cmd
            elif 'ansible_ssh_private_key_file' in config:
                key_file = config['ansible_ssh_private_key_file']
                if key_file.startswith('~'):
                    key_file = os.path.expanduser(key_file)
                ssh_cmd.extend(['-i', key_file])
            
            # Add SSH options
            ssh_cmd.extend([
                '-o', 'StrictHostKeyChecking=no',
                '-o', 'UserKnownHostsFile=/dev/null',
                '-o', 'ConnectTimeout=10',
                f"{user}@{ip}",
                'cat /var/cache/edge_node/_local_cache/_data/e2.pem'
            ])
            
            try:
                # Execute SSH command to get the private key
                result = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=30)
                
                if result.returncode == 0 and result.stdout.strip():
                    # Save the private key to file
                    key_filename = f"{host_name}_e2.pem"
                    key_filepath = os.path.join(keys_dir, key_filename)
                    
                    with open(key_filepath, 'w') as f:
                        f.write(result.stdout)
                    
                    # Set restrictive permissions on the key file
                    os.chmod(key_filepath, 0o600)
                    
                    self.print_colored(f"✅ Successfully imported key: {key_filename}", 'green')
                    successful_imports += 1
                else:
                    error_msg = result.stderr.strip() if result.stderr else "No key content received"
                    self.print_colored(f"❌ Failed to retrieve key: {error_msg}", 'red')
                    failed_imports += 1
                    
            except subprocess.TimeoutExpired:
                self.print_colored(f"❌ Connection timeout to {host_name}", 'red')
                failed_imports += 1
            except FileNotFoundError as e:
                if 'sshpass' in str(e):
                    self.print_colored(f"❌ sshpass not found - install with: sudo apt-get install sshpass", 'red')
                else:
                    self.print_colored(f"❌ SSH command failed: {e}", 'red')
                failed_imports += 1
            except Exception as e:
                self.print_colored(f"❌ Error processing {host_name}: {e}", 'red')
                failed_imports += 1
        
        # Summary
        self.print_section("Import Summary")
        self.print_colored(f"✅ Successful imports: {successful_imports}", 'green')
        self.print_colored(f"❌ Failed imports: {failed_imports}", 'red')
        self.print_colored(f"📁 Keys saved to: {keys_dir}", 'cyan')
        
        if successful_imports > 0:
            self.print_colored("\n🔐 SECURITY REMINDER:", 'red', bold=True)
            self.print_colored("  • Private keys have been saved locally", 'red')
            self.print_colored("  • Keep these files secure and delete when no longer needed", 'red')
            self.print_colored("  • These keys provide full access to node wallets", 'red')
        
        input("\nPress Enter to continue...")

    def _create_initial_configuration(self) -> None:
        """Create initial configuration"""
        self.print_section("Initial Configuration Setup")

        # First step: Get configuration name
        self.print_colored("\n📝 Configuration Naming", 'cyan', bold=True)
        self.print_colored("Give your configuration a meaningful name to identify it later.", 'yellow')
        self.print_colored("Examples: 'production-cluster', 'test-env', 'gpu-farm-1'", 'white')

        while True:
            custom_name = self.get_input("Enter configuration name", required=True)
            # Validate name (allow letters, numbers, hyphens, underscores)
            if re.match(r'^[a-zA-Z0-9_-]+$', custom_name):
                break
            self.print_colored("Invalid name. Use only letters, numbers, hyphens (-), and underscores (_)", 'red')

        # Select network environment
        env = self._select_network_environment()
        self.set_mnl_app_env(env)

        # Get number of nodes
        while True:
            try:
                num_nodes = int(self.get_input("How many nodes do you want to configure", "1"))
                if num_nodes <= 0:
                    self.print_colored("Please enter a positive number", 'red')
                    continue
                break
            except ValueError:
                self.print_colored("Please enter a valid number", 'red')

        # Generate configuration name with the custom name
        config_name = self._generate_config_name(num_nodes, custom_name)

        # Configure each node
        hosts = self.inventory['all']['children']['gpu_nodes']['hosts']
        for i in range(num_nodes):
            self.print_section(f"Configuring Node {i + 1} of {num_nodes}")
            while True:
                name = self._get_valid_hostname(f"Enter name for node {i + 1}", f"gpu-node-{i + 1}")
                if name in hosts:
                    self.print_colored(f"Node '{name}' already exists! Please choose a different name.", 'red')
                    continue
                break
            hosts[name] = self._configure_single_node()
            self.print_colored(f"Node '{name}' configured successfully!", 'green')

        # Save configuration with metadata (instead of legacy _save_configuration)
        self._save_config_with_metadata(config_name, env, num_nodes)
        self.print_colored(f"Configuration '{config_name}' created and activated!", 'green')

    def _select_network_environment(self) -> str:
        """Select network environment"""
        self.print_colored("\nNetwork Environment Options:")
        self.print_colored("  1) mainnet")
        self.print_colored("  2) testnet")
        self.print_colored("  3) devnet")

        current_env = self.get_mnl_app_env()
        if current_env:
            self.print_colored(f"Current: {current_env}", 'yellow')

        while True:
            choice = self.get_input("Select network environment (1-3)", "1")
            if choice == '1':
                return 'mainnet'
            elif choice == '2':
                return 'testnet'
            elif choice == '3':
                return 'devnet'
            else:
                self.print_colored("Invalid choice. Please enter 1, 2, or 3", 'red')

    def _configure_single_node(self, existing_config: Dict[str, Any] = None) -> Dict[str, Any]:
        """Configure a single node"""
        while True:
            host = {}

            # Get existing values for defaults
            existing_ip = existing_config.get('ansible_host', '') if existing_config else ''
            existing_user = existing_config.get('ansible_user', '') if existing_config else ''
            existing_auth_type = 'password' if existing_config and 'ansible_ssh_pass' in existing_config else 'key' if existing_config else None

            # Show current values if updating
            if existing_config:
                self.print_colored("\nCurrent configuration:", 'yellow')
                self.print_colored(f"Host: {existing_ip}")
                self.print_colored(f"User: {existing_user}")
                self.print_colored(f"Auth: {'Password' if existing_auth_type == 'password' else 'SSH Key'}")
                self.print_colored("\nPress Enter to keep current values, or enter new values:", 'cyan')

            # Get host (IP address or URL)
            host_prompt = f"Enter host (IP address or URL)"
            if existing_ip:
                host_prompt += f" (current: {existing_ip})"
            host_address = self.get_input(host_prompt, existing_ip, required=True)
            host['ansible_host'] = host_address

            # Get username
            user_prompt = f"Enter SSH username"
            if existing_user:
                user_prompt += f" (current: {existing_user})"
            username = self.get_input(user_prompt, existing_user)
            if not username.strip() and not existing_user:
                self.print_colored("Username cannot be empty for new nodes", 'red')
                continue
            host['ansible_user'] = username or existing_user

            # Authentication method selection (with retry loop)
            auth_configured = False
            while not auth_configured:
                # Authentication method
                self.print_colored("\nAuthentication method:")
                self.print_colored("  1) Password")
                self.print_colored("  2) SSH Key")

                # Set default based on existing config
                default_auth = "1" if existing_auth_type == 'password' else "2"
                auth_prompt = "Select authentication (1/2)"
                if existing_auth_type:
                    auth_prompt += f" (current: {existing_auth_type})"

                while True:
                    auth_choice = self.get_input(auth_prompt, default_auth)
                    if auth_choice in ['1', '2']:
                        break
                    self.print_colored("Invalid choice. Please enter 1 or 2", 'red')

                if auth_choice == '1':
                    # Password authentication
                    if existing_config and 'ansible_ssh_pass' in existing_config:
                        self.print_colored("\nPassword authentication - Press Enter to keep existing passwords", 'cyan')
                        ssh_pass = self.get_secure_input("Enter SSH password (Enter to keep current)")
                        if not ssh_pass.strip():
                            # Keep existing password
                            host['ansible_ssh_pass'] = existing_config['ansible_ssh_pass']
                            host['ansible_become_password'] = existing_config.get('ansible_become_password', existing_config['ansible_ssh_pass'])
                        else:
                            # New password provided
                            host['ansible_ssh_pass'] = ssh_pass
                            self.print_colored("\nFor sudo password:", 'yellow')
                            self.print_colored("  - Enter a different password if sudo requires it", 'yellow')
                            self.print_colored("  - Press Enter to use the same SSH password", 'yellow')
                            sudo_pass = self.get_secure_input("Enter sudo password")
                            host['ansible_become_password'] = sudo_pass.strip() or ssh_pass
                    else:
                        # New password authentication
                        ssh_pass = self.get_secure_input("Enter SSH password")
                        if not ssh_pass.strip():
                            self.print_colored("SSH password cannot be empty!", 'red')
                            continue  # This will go back to authentication method selection
                        host['ansible_ssh_pass'] = ssh_pass

                        self.print_colored("\nFor sudo password:", 'yellow')
                        self.print_colored("  - Enter a different password if sudo requires it", 'yellow')
                        self.print_colored("  - Press Enter to use the same SSH password", 'yellow')
                        sudo_pass = self.get_secure_input("Enter sudo password")
                        host['ansible_become_password'] = sudo_pass.strip() or ssh_pass
                    
                    auth_configured = True
                else:
                    # Key authentication
                    existing_key = existing_config.get('ansible_ssh_private_key_file', '~/.ssh/id_rsa') if existing_config else '~/.ssh/id_rsa'

                    key_auth_success = False
                    while not key_auth_success:
                        key_prompt = "Enter path to SSH private key"
                        if existing_config and 'ansible_ssh_private_key_file' in existing_config:
                            key_prompt += f" (current: {existing_key})"

                        key_path = self.get_input(key_prompt, existing_key)
                        expanded_path = os.path.expanduser(key_path)
                        
                        # Validate SSH key file
                        validation_result = self._validate_ssh_key_file(expanded_path)
                        if validation_result['valid']:
                            host['ansible_ssh_private_key_file'] = key_path
                            
                            # Prompt for sudo password
                            if existing_config and 'ansible_become_password' in existing_config:
                                self.print_colored("\nFor sudo password (Press Enter to keep existing):", 'yellow')
                                self.print_colored("  - Enter a sudo password if required", 'yellow')
                                self.print_colored("  - Press Enter to keep existing password", 'yellow')
                                self.print_colored("  - Type 'none' if sudo doesn't require a password", 'yellow')
                                sudo_pass = self.get_secure_input("Enter sudo password")
                                if sudo_pass.strip().lower() == 'none':
                                    # User explicitly said no sudo password needed
                                    pass  # Don't set ansible_become_password
                                elif sudo_pass.strip():
                                    host['ansible_become_password'] = sudo_pass.strip()
                                else:
                                    # Keep existing
                                    host['ansible_become_password'] = existing_config['ansible_become_password']
                            else:
                                # New configuration
                                self.print_colored("\nFor sudo password:", 'yellow')
                                self.print_colored("  - Enter a sudo password if required", 'yellow')
                                self.print_colored("  - Press Enter if sudo doesn't require a password", 'yellow')
                                sudo_pass = self.get_secure_input("Enter sudo password")
                                if sudo_pass.strip():
                                    host['ansible_become_password'] = sudo_pass.strip()
                            
                            key_auth_success = True
                            auth_configured = True
                            break
                        
                        # Show specific error message
                        self.print_colored(f"SSH key validation failed: {validation_result['error']}", 'red')
                        self.print_colored(f"Path checked: {expanded_path}", 'yellow')
                        
                        retry_choice = self.get_input("Choose an option:\n  1) Try another SSH key path\n  2) Switch to password authentication\n  3) Keep existing (if updating)\nSelect option (1/2/3)", "1")
                        
                        if retry_choice == '1':
                            continue  # Try another path
                        elif retry_choice == '2':
                            self.print_colored("Switching to password authentication...", 'cyan')
                            break  # Exit SSH key loop, will go back to auth method selection
                        elif retry_choice == '3' and existing_config and 'ansible_ssh_private_key_file' in existing_config:
                            # Keep existing key configuration
                            host['ansible_ssh_private_key_file'] = existing_config['ansible_ssh_private_key_file']
                            # Also keep existing sudo password if present
                            if 'ansible_become_password' in existing_config:
                                host['ansible_become_password'] = existing_config['ansible_become_password']
                            self.print_colored("Keeping existing SSH key configuration", 'yellow')
                            key_auth_success = True
                            auth_configured = True
                            break
                        else:
                            if retry_choice == '3':
                                self.print_colored("No existing configuration to keep. Please choose option 1 or 2.", 'red')
                            else:
                                self.print_colored("Invalid choice. Please select 1, 2, or 3.", 'red')
                    
                    # If user chose to switch to password auth, continue the auth loop
                    if not key_auth_success and not auth_configured:
                        continue

            # Preserve or set SSH common args
            host['ansible_ssh_common_args'] = existing_config.get('ansible_ssh_common_args', '-o StrictHostKeyChecking=no') if existing_config else '-o StrictHostKeyChecking=no'

            # Initialize or preserve node status fields
            if existing_config:
                # Preserve existing status unless it's being updated
                host['node_status'] = existing_config.get('node_status', 'unknown')
                host['last_status_update'] = existing_config.get('last_status_update', datetime.now().isoformat())
            else:
                # New node - set initial status
                timestamp = datetime.now().isoformat()
                host['node_status'] = 'never_deployed'
                host['last_status_update'] = timestamp

            # Show summary and confirm
            self.print_colored("\nConfiguration Summary:", 'yellow')
            self.print_colored(f"Host: {host['ansible_host']}")
            self.print_colored(f"User: {host['ansible_user']}")
            self.print_colored(f"Auth: {'Password' if auth_choice == '1' else 'SSH Key'}")

            if self.get_input("\nConfirm this configuration? (y/n)", "y").lower() == 'y':
                return host

            self.print_colored("Let's reconfigure this node...", 'yellow')

    def _should_preserve_node_status(self, node_name: str) -> bool:
        """Check if a node's status should be preserved (e.g., deleted nodes)"""
        current_status = self._get_node_status_info(node_name)['status']
        return current_status == 'deleted'

    def _update_node_status(self, node_name: str, status: str) -> None:
        """Update the status of a specific node"""
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        if node_name in hosts:
            old_status = hosts[node_name].get('node_status', 'unknown')
            
            # Preserve certain statuses (e.g., deleted nodes)
            if self._should_preserve_node_status(node_name) and status not in ['deploying', 'error']:
                self.print_debug(f"Preserving {node_name} status: {old_status} (requested: {status})")
                return
            
            old_update = hosts[node_name].get('last_status_update', 'never')
            new_update = datetime.now().isoformat()
            
            hosts[node_name]['node_status'] = status
            hosts[node_name]['last_status_update'] = new_update
            self._save_configuration()
            
            self.print_debug(f"Updated {node_name} status: {old_status} → {status}")
            self.print_debug(f"  Previous update: {old_update}")
            self.print_debug(f"  New update: {new_update}")

    def _get_node_status_info(self, node_name: str) -> Dict[str, str]:
        """Get status information for a node"""
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        if node_name in hosts:
            status = hosts[node_name].get('node_status', 'unknown')
            last_update = hosts[node_name].get('last_status_update', '')
            self.print_debug(f"Retrieved {node_name} status: {status} (updated: {last_update})")
            return {'status': status, 'last_update': last_update}
        self.print_debug(f"Node {node_name} not found in hosts configuration")
        return {'status': 'unknown', 'last_update': ''}

    def _get_real_time_node_status(self) -> Dict[str, Dict[str, str]]:
        """Get real-time status for all nodes by running the service status playbook"""
        # Run service status playbook
        playbook_path = self.config_dir / 'playbooks/service_status.yml'
        if not playbook_path.exists():
            self.print_colored(f"❌ Service status playbook not found: {playbook_path}", 'red')
            return {}

        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} {playbook_path}")

        # Use a reasonable timeout for status checking (30 seconds should be enough for most cases)
        success, output = self.run_command(cmd, show_output=False, timeout=30)
        
        # Process results for real-time status
        node_status_data = {}
        
        if success:
            self.print_debug(f"Playbook output length: {len(output)} characters")
            self.print_debug("=== PLAYBOOK OUTPUT ===")
            self.print_debug(output)
            self.print_debug("=== END OUTPUT ===")
            
            # Parse the output for service status information
            lines = output.split('\n')
            current_host = None
            
            # Use regex to find all task result blocks
            import re
        else:
            # Handle timeout or other failure cases
            self.print_debug(f"Status check failed with output length: {len(output) if output else 0}")
            if "timed out" in output.lower():
                self.print_debug("Status check timed out - some hosts may be offline")
            else:
                self.print_debug(f"Status check failed with error: {output}")
            
            # We'll still try to parse whatever output we got - partial output is valuable
            if output:
                self.print_debug("=== PARTIAL PLAYBOOK OUTPUT ===")
                self.print_debug(output)
                self.print_debug("=== END PARTIAL OUTPUT ===")
            
            lines = output.split('\n') if output else []
            current_host = None
            
            # Use regex to find all task result blocks
            import re
            
            # Look for pattern: ok: [hostname] => {
            task_pattern = re.compile(r'ok: \[([^\]]+)\] => \{')
            
            for i, line in enumerate(lines):
                line = line.strip()
                
                # Track current host being processed from task results
                task_match = task_pattern.match(line)
                if task_match:
                    current_host = task_match.group(1)
                    self.print_debug(f"Found task result for host: {current_host}")
                    continue
                
                # Handle unreachable nodes - both patterns
                if 'unreachable:' in line and '[' in line and ']' in line:
                    unreachable_match = re.search(r'unreachable: \[([^\]]+)\]', line)
                    if unreachable_match:
                        hostname = unreachable_match.group(1)
                        node_status_data[hostname] = {
                            'status': 'unreachable',
                            'result': 'Node unreachable'
                        }
                        self.print_debug(f"Set {hostname} to unreachable")
                        continue
                
                # Handle fatal unreachable nodes (ansible format: fatal: [hostname]: UNREACHABLE!)
                if 'fatal:' in line and 'UNREACHABLE!' in line and '[' in line and ']' in line:
                    fatal_match = re.search(r'fatal: \[([^\]]+)\]: UNREACHABLE!', line)
                    if fatal_match:
                        hostname = fatal_match.group(1)
                        node_status_data[hostname] = {
                            'status': 'unreachable',
                            'result': 'Node unreachable - connection failed'
                        }
                        self.print_debug(f"Set {hostname} to unreachable (fatal)")
                        continue
                
                # Look for the structured message output from the playbook
                if current_host and '"msg":' in line:
                    self.print_debug(f"Processing message line for {current_host}: {line}")
                    
                    # Extract the message content - it contains the status information
                    msg_match = re.search(r'"msg":\s*"([^"]*(?:\\.[^"]*)*)"', line)
                    if msg_match:
                        msg_content = msg_match.group(1)
                        # Decode escaped characters (like \n)
                        msg_content = msg_content.replace('\\n', '\n').replace('\\"', '"')
                        self.print_debug(f"Extracted message for {current_host}: {msg_content}")
                        
                        # If the message is truncated, try to get the full message from subsequent lines
                        if msg_content.endswith('\\n') or not msg_content.endswith('"'):
                            self.print_debug(f"Message appears truncated for {current_host}, looking for continuation")
                            # Look ahead for more message content
                            for j in range(i + 1, min(i + 10, len(lines))):
                                next_line = lines[j].strip()
                                if next_line.startswith('"') and next_line.endswith('"'):
                                    # End of message
                                    msg_content += next_line[1:-1].replace('\\n', '\n').replace('\\"', '"')
                                    break
                                elif next_line.startswith('"'):
                                    # Continuation of message
                                    msg_content += next_line[1:].replace('\\n', '\n').replace('\\"', '"')
                                else:
                                    # Not part of message
                                    break
                            self.print_debug(f"Complete message for {current_host}: {msg_content}")
                        
                        # Check if this message contains status information
                        if 'Service Status:' in msg_content or 'Container Status:' in msg_content:
                            self.print_debug(f"Found status information in message for {current_host}")
                        else:
                            self.print_debug(f"No status information found in message for {current_host}")
                            continue
                        
                        # Parse the decoded message content for status information
                        service_status = None
                        container_status = None
                        
                        # Look for Service Status in the message
                        if 'Service Status: ACTIVE' in msg_content:
                            service_status = 'ACTIVE'
                            self.print_debug(f"Found ACTIVE service status for {current_host}")
                        elif 'Service Status: INACTIVE' in msg_content or 'Service Status: INACTIVE/FAILED' in msg_content:
                            service_status = 'INACTIVE'
                            self.print_debug(f"Found INACTIVE service status for {current_host}")
                        elif 'Service Status: FAILED' in msg_content:
                            service_status = 'FAILED'
                            self.print_debug(f"Found FAILED service status for {current_host}")
                        elif 'Service Status: NOT FOUND' in msg_content:
                            service_status = 'NOT_FOUND'
                            self.print_debug(f"Found NOT_FOUND service status for {current_host}")
                        else:
                            self.print_debug(f"No service status found in message for {current_host}")
                        
                        # Look for Container Status in the message
                        if 'Container Status: RUNNING' in msg_content:
                            container_status = 'RUNNING'
                            self.print_debug(f"Found RUNNING container status for {current_host}")
                        elif 'Container Status: NOT RUNNING' in msg_content:
                            container_status = 'NOT_RUNNING'
                            self.print_debug(f"Found NOT_RUNNING container status for {current_host}")
                        else:
                            self.print_debug(f"No container status found in message for {current_host}")
                        
                        self.print_debug(f"Parsed {current_host}: service={service_status}, container={container_status}")
                        
                        # Determine overall status based on service and container status
                        self.print_debug(f"Determining status for {current_host}: container={container_status}, service={service_status}")
                        if container_status == 'RUNNING':
                            node_status_data[current_host] = {
                                'status': 'running',
                                'result': 'Container is running'
                            }
                            self.print_debug(f"Set {current_host} to running (container running)")
                        elif container_status == 'NOT_RUNNING':
                            if service_status == 'ACTIVE':
                                node_status_data[current_host] = {
                                    'status': 'stopped',
                                    'result': 'Service active but container not running'
                                }
                                self.print_debug(f"Set {current_host} to stopped (service active, container not running)")
                            elif service_status == 'NOT_FOUND':
                                node_status_data[current_host] = {
                                    'status': 'not_deployed',
                                    'result': 'Service not found - not deployed'
                                }
                                self.print_debug(f"Set {current_host} to not_deployed (service not found)")
                            else:
                                node_status_data[current_host] = {
                                    'status': 'stopped',
                                    'result': 'Container not running'
                                }
                                self.print_debug(f"Set {current_host} to stopped (container not running)")
                        elif service_status == 'ACTIVE' and current_host not in node_status_data:
                            node_status_data[current_host] = {
                                'status': 'running',
                                'result': 'Service is active'
                            }
                            self.print_debug(f"Set {current_host} to running (service active)")
                        elif service_status in ['INACTIVE', 'FAILED'] and current_host not in node_status_data:
                            node_status_data[current_host] = {
                                'status': 'stopped',
                                'result': 'Service is inactive/failed'
                            }
                            self.print_debug(f"Set {current_host} to stopped (service inactive/failed)")
                        elif service_status == 'NOT_FOUND' and current_host not in node_status_data:
                            node_status_data[current_host] = {
                                'status': 'not_deployed',
                                'result': 'Service not found - not deployed'
                            }
                            self.print_debug(f"Set {current_host} to not_deployed (service not found)")
                
                # Look for status information in the task output directly (new format)
                elif current_host and ('Service Status:' in line or 'Container Status:' in line):
                    self.print_debug(f"Processing status line for {current_host}: {line}")
                    
                    # Parse service status
                    if 'Service Status: ACTIVE' in line:
                        service_status = 'ACTIVE'
                    elif 'Service Status: INACTIVE' in line or 'Service Status: INACTIVE/FAILED' in line:
                        service_status = 'INACTIVE'
                    elif 'Service Status: FAILED' in line:
                        service_status = 'FAILED'
                    elif 'Service Status: NOT FOUND' in line:
                        service_status = 'NOT_FOUND'
                    
                    # Parse container status
                    if 'Container Status: RUNNING' in line:
                        container_status = 'RUNNING'
                    elif 'Container Status: NOT RUNNING' in line:
                        container_status = 'NOT_RUNNING'
                    
                    self.print_debug(f"Parsed {current_host}: service={service_status}, container={container_status}")
                    
                    # Determine overall status based on service and container status
                    if container_status == 'RUNNING':
                        node_status_data[current_host] = {
                            'status': 'running',
                            'result': 'Container is running'
                        }
                        self.print_debug(f"Set {current_host} to running (container running)")
                    elif container_status == 'NOT_RUNNING':
                        if service_status == 'ACTIVE':
                            node_status_data[current_host] = {
                                'status': 'stopped',
                                'result': 'Service active but container not running'
                            }
                            self.print_debug(f"Set {current_host} to stopped (service active, container not running)")
                        elif service_status == 'NOT_FOUND':
                            node_status_data[current_host] = {
                                'status': 'not_deployed',
                                'result': 'Service not found - not deployed'
                            }
                            self.print_debug(f"Set {current_host} to not_deployed (service not found)")
                        else:
                            node_status_data[current_host] = {
                                'status': 'stopped',
                                'result': 'Container not running'
                            }
                            self.print_debug(f"Set {current_host} to stopped (container not running)")
                    elif service_status == 'ACTIVE' and current_host not in node_status_data:
                        node_status_data[current_host] = {
                            'status': 'running',
                            'result': 'Service is active'
                        }
                        self.print_debug(f"Set {current_host} to running (service active)")
                    elif service_status in ['INACTIVE', 'FAILED'] and current_host not in node_status_data:
                        node_status_data[current_host] = {
                            'status': 'stopped',
                            'result': 'Service is inactive/failed'
                        }
                        self.print_debug(f"Set {current_host} to stopped (service inactive/failed)")
                    elif service_status == 'NOT_FOUND' and current_host not in node_status_data:
                        node_status_data[current_host] = {
                            'status': 'not_deployed',
                            'result': 'Service not found - not deployed'
                        }
                        self.print_debug(f"Set {current_host} to not_deployed (service not found)")
                
                # Fallback: Look for debug output with container and service status (legacy format)
                elif current_host and ('Container Status:' in line or 'Service Status:' in line):
                    # Parse container status
                    if 'Container Status: RUNNING' in line:
                        node_status_data[current_host] = {
                            'status': 'running',
                            'result': 'Container is running'
                        }
                        self.print_debug(f"Set {current_host} to running (legacy format)")
                    elif 'Container Status: NOT RUNNING' in line:
                        # Check if we have service status info
                        if 'Service Status: ACTIVE' in line:
                            node_status_data[current_host] = {
                                'status': 'stopped',
                                'result': 'Service active but container not running'
                            }
                        elif 'Service Status: NOT FOUND' in line:
                            node_status_data[current_host] = {
                                'status': 'not_deployed',
                                'result': 'Service not found - not deployed'
                            }
                        else:
                            node_status_data[current_host] = {
                                'status': 'stopped',
                                'result': 'Container not running'
                            }
                        self.print_debug(f"Set {current_host} to stopped (legacy format)")
                    
                    # Parse service status when container info is not available
                    elif 'Service Status: ACTIVE' in line and current_host not in node_status_data:
                        node_status_data[current_host] = {
                            'status': 'running',
                            'result': 'Service is active'
                        }
                        self.print_debug(f"Set {current_host} to running (service active, legacy format)")
                    elif 'Service Status: INACTIVE' in line or 'Service Status: FAILED' in line or 'Service Status: INACTIVE/FAILED' in line:
                        node_status_data[current_host] = {
                            'status': 'stopped',
                            'result': 'Service is inactive/failed'
                        }
                        self.print_debug(f"Set {current_host} to stopped (service inactive/failed, legacy format)")
                    elif 'Service Status: NOT FOUND' in line:
                        node_status_data[current_host] = {
                            'status': 'not_deployed',
                            'result': 'Service not found - not deployed'
                        }
                        self.print_debug(f"Set {current_host} to not_deployed (service not found, legacy format)")
        
        self.print_debug(f"Final node status data: {node_status_data}")
        
        # If we still don't have status data, try a more aggressive approach
        if not node_status_data:
            self.print_debug("No status data found, trying aggressive parsing approach")
            # Look for any lines containing status information in the entire output
            for i, line in enumerate(lines):
                line = line.strip()
                
                # Look for any host mention
                host_match = re.search(r'ok:\s*\[([^\]]+)\]', line)
                if host_match:
                    current_host = host_match.group(1)
                    self.print_debug(f"Found host mention: {current_host}")
                
                # Look for status information anywhere in the output
                if current_host and ('Service Status:' in line or 'Container Status:' in line):
                    self.print_debug(f"Found status line for {current_host}: {line}")
                    
                    # Parse service status
                    service_status = None
                    if 'Service Status: ACTIVE' in line:
                        service_status = 'ACTIVE'
                    elif 'Service Status: INACTIVE' in line or 'Service Status: INACTIVE/FAILED' in line:
                        service_status = 'INACTIVE'
                    elif 'Service Status: FAILED' in line:
                        service_status = 'FAILED'
                    elif 'Service Status: NOT FOUND' in line:
                        service_status = 'NOT_FOUND'
                    
                    # Parse container status
                    container_status = None
                    if 'Container Status: RUNNING' in line:
                        container_status = 'RUNNING'
                    elif 'Container Status: NOT RUNNING' in line:
                        container_status = 'NOT_RUNNING'
                    
                    self.print_debug(f"Aggressive parsing for {current_host}: service={service_status}, container={container_status}")
                    
                    # Determine overall status based on service and container status
                    if container_status == 'RUNNING':
                        node_status_data[current_host] = {
                            'status': 'running',
                            'result': 'Container is running'
                        }
                        self.print_debug(f"Set {current_host} to running (container running)")
                    elif container_status == 'NOT_RUNNING':
                        if service_status == 'ACTIVE':
                            node_status_data[current_host] = {
                                'status': 'stopped',
                                'result': 'Service active but container not running'
                            }
                            self.print_debug(f"Set {current_host} to stopped (service active, container not running)")
                        elif service_status == 'NOT_FOUND':
                            node_status_data[current_host] = {
                                'status': 'not_deployed',
                                'result': 'Service not found - not deployed'
                            }
                            self.print_debug(f"Set {current_host} to not_deployed (service not found)")
                        else:
                            node_status_data[current_host] = {
                                'status': 'stopped',
                                'result': 'Container not running'
                            }
                            self.print_debug(f"Set {current_host} to stopped (container not running)")
                    elif service_status == 'ACTIVE' and current_host not in node_status_data:
                        node_status_data[current_host] = {
                            'status': 'running',
                            'result': 'Service is active'
                        }
                        self.print_debug(f"Set {current_host} to running (service active)")
                    elif service_status in ['INACTIVE', 'FAILED'] and current_host not in node_status_data:
                        node_status_data[current_host] = {
                            'status': 'stopped',
                            'result': 'Service is inactive/failed'
                        }
                        self.print_debug(f"Set {current_host} to stopped (service inactive/failed)")
                    elif service_status == 'NOT_FOUND' and current_host not in node_status_data:
                        node_status_data[current_host] = {
                            'status': 'not_deployed',
                            'result': 'Service not found - not deployed'
                        }
                        self.print_debug(f"Set {current_host} to not_deployed (service not found)")
        
        # Fill in any missing hosts with unknown status
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        for hostname in hosts.keys():
            if hostname not in node_status_data:
                node_status_data[hostname] = {
                    'status': 'unknown',
                    'result': 'Unable to determine status'
                }
        
        return node_status_data

    def _get_status_display_info(self, status: str) -> Tuple[str, str, str]:
        """Get display information for a status (emoji, color, description)"""
        status_info = {
            'running': ('🟢', 'green', 'Running'),
            'stopped': ('🔴', 'red', 'Stopped'),
            'pending_restart': ('🟡', 'yellow', 'Pending Restart'),
            'unknown': ('❓', 'white', 'Unknown'),
            'deploying': ('🔄', 'cyan', 'Deploying'),
            'error': ('❌', 'red', 'Error'),
            'never_deployed': ('⚪', 'white', 'Never Deployed'),
            'deleted': ('🗑️', 'red', 'Deleted'),
            'unreachable': ('🔌', 'red', 'Unreachable'),
            'not_deployed': ('📦', 'yellow', 'Not Deployed')
        }
        return status_info.get(status, ('❓', 'white', 'Unknown'))

    def _display_node_status(self, node_name: str, compact: bool = False) -> None:
        """Display the status of a node"""
        status_info = self._get_node_status_info(node_name)
        status = status_info['status']
        emoji, color, description = self._get_status_display_info(status)
        
        if compact:
            self.print_colored(f"{emoji} {description}", color, end='')
        else:
            self.print_colored(f"Status: {emoji} {description}", color)

    def _add_node(self) -> None:
        """Add a new node to existing configuration"""
        self.print_section("Add New Node")
        hosts = self.inventory['all']['children']['gpu_nodes']['hosts']

        while True:
            name = self._get_valid_hostname("Enter name for the new node", "")
            if name in hosts:
                self.print_colored(f"You already have a node named '{name}'! Please choose a different name.", 'red')
                continue
            break

        hosts[name] = self._configure_single_node()
        self._save_configuration()
        self.print_colored(f"Node '{name}' added successfully!", 'green')

    def _update_node(self) -> None:
        """Update an existing node"""
        hosts = self.inventory['all']['children']['gpu_nodes']['hosts']
        if not hosts:
            self.print_colored("No nodes configured!", 'red')
            return

        self.print_section("Select Node to Update")
        node_list = list(hosts.keys())
        for i, name in enumerate(node_list, 1):
            ip = hosts[name].get('ansible_host', 'Unknown')
            self.print_colored(f"  {i}) {name} ({ip})")

        while True:
            try:
                choice = int(self.get_input("Select node number", "1")) - 1
                if 0 <= choice < len(node_list):
                    original_name = node_list[choice]
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        self.print_colored(f"Updating node: {original_name}", 'yellow')

        # Ask if user wants to rename the node
        rename_node = self.get_input(f"\nDo you want to rename this node? Current name: '{original_name}' (y/n)", "n").lower() == 'y'

        new_name = original_name
        if rename_node:
            while True:
                new_name = self._get_valid_hostname(f"Enter new name for node (current: {original_name})", original_name)
                if new_name == original_name:
                    self.print_colored("New name is the same as current name.", 'yellow')
                    break
                if new_name in hosts:
                    self.print_colored(f"Node name '{new_name}' already exists! Please choose a different name.", 'red')
                    continue
                break

        # Update node configuration
        existing_config = hosts[original_name].copy()
        updated_config = self._configure_single_node(existing_config)

        # Handle name change
        name_changed = new_name != original_name
        if name_changed:
            # Remove old entry and add new one
            del hosts[original_name]
            hosts[new_name] = updated_config
            # Set status to pending_restart for renamed nodes
            self._update_node_status(new_name, 'pending_restart')
            self.print_colored(f"Node renamed from '{original_name}' to '{new_name}'", 'green')
        else:
            # Update existing entry
            hosts[original_name] = updated_config

        self._save_configuration()

        if name_changed:
            self.print_colored(f"Node '{original_name}' updated and renamed to '{new_name}' successfully!", 'green')
            self.print_colored(f"Status: ", 'cyan', end='')
            self._display_node_status(new_name, compact=True)
            print()  # New line after status
            self.print_colored("\n💡 Recommendation:", 'cyan', bold=True)
            self.print_colored("Since you changed the node name, the node status is now 'Pending Restart'.", 'yellow')
            self.print_colored("Use option 13 (Restart Edge Node) from the main menu to update the status.", 'yellow')
            self.print_colored(f"When prompted, select ONLY the renamed node '{new_name}' to avoid", 'yellow')
            self.print_colored("disturbing other running nodes. This will ensure the renamed node", 'yellow')
            self.print_colored("starts with its updated configuration.", 'white')
        else:
            self.print_colored(f"Node '{original_name}' updated successfully!", 'green')

    def _delete_node(self) -> None:
        """Delete a node"""
        hosts = self.inventory['all']['children']['gpu_nodes']['hosts']
        if not hosts:
            self.print_colored("No nodes configured!", 'red')
            return

        self.print_section("Select Node to Delete")
        node_list = list(hosts.keys())
        for i, name in enumerate(node_list, 1):
            ip = hosts[name].get('ansible_host', 'Unknown')
            self.print_colored(f"  {i}) {name} ({ip})")

        while True:
            try:
                choice = int(self.get_input("Select node number", "1")) - 1
                if 0 <= choice < len(node_list):
                    name = node_list[choice]
                    break
                self.print_colored("Invalid selection", 'red')
            except ValueError:
                self.print_colored("Please enter a number", 'red')

        if self.get_input(f"Delete node '{name}'? (y/n)", "n").lower() == 'y':
            del hosts[name]
            self._save_configuration()
            self.print_colored(f"Node '{name}' deleted successfully!", 'green')

    def _create_new_configuration(self) -> None:
        """Create completely new configuration"""
        if self.get_input("This will overwrite your current configuration. Continue? (y/n)", "n").lower() == 'y':
            # Backup existing config
            if self.config_file.exists():
                backup_dir = self.config_dir / 'hosts-history'
                backup_dir.mkdir(exist_ok=True)
                timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
                backup_file = backup_dir / f'hosts-{timestamp}.yml'
                self.config_file.rename(backup_file)
                self.print_colored(f"Configuration backed up to: {backup_file}", 'green')

            # Reset inventory
            self.inventory = {
                'all': {
                    'vars': {},
                    'children': {
                        'gpu_nodes': {
                            'hosts': {}
                        }
                    }
                }
            }
            self._create_initial_configuration()

    def _save_configuration(self) -> None:
        """Save configuration to file (legacy method - now uses new config management)"""
        # Get current environment and node count
        env = self.get_mnl_app_env() or 'mainnet'
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        nodes_count = len(hosts)

        # Generate config name if not set
        if not self.active_config.get('config_name'):
            config_name = self._generate_config_name(nodes_count)
            self._save_config_with_metadata(config_name, env, nodes_count, update_symlink=True)
        else:
            # Update existing config - no need to update symlink since we're not switching configs
            config_name = self.active_config['config_name']
            self._save_config_with_metadata(config_name, env, nodes_count, update_symlink=False)

    def view_configuration(self) -> None:
        """View current configuration"""
        self.print_header("Current Configuration")

        # Show active configuration info
        active_config_name = self.active_config.get('config_name')
        if active_config_name:
            self.print_section("Active Configuration")
            self.print_colored(f"Configuration Name: {active_config_name}", 'green')
            self.print_colored(f"Environment: {self.active_config.get('environment', 'Unknown')}", 'green')
            self.print_colored(f"Nodes Count: {self.active_config.get('nodes_count', 0)}", 'green')
            created_at = self.active_config.get('created_at')
            if created_at:
                try:
                    created_dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
                    created_str = created_dt.strftime('%Y-%m-%d %H:%M')
                    self.print_colored(f"Created: {created_str}", 'green')
                except:
                    self.print_colored(f"Created: {created_at}", 'green')
        else:
            self.print_colored("No active configuration", 'red')

        # Show network environment
        env = self.get_mnl_app_env()
        self.print_colored(f"\nCurrent Network Environment: {env if env else 'Not set'}",
                           'green' if env else 'red')

        # Load and show hosts
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})

        if not hosts:
            self.print_colored("\nNo nodes configured!", 'red')
        else:
            self.print_section(f"Configured Nodes ({len(hosts)})")
            for name, config in hosts.items():
                # Get status information
                status_info = self._get_node_status_info(name)
                status = status_info['status']
                status_emoji, status_color, status_desc = self._get_status_display_info(status)
                
                self.print_colored(f"\nNode: {name} ", 'yellow', end='')
                self.print_colored(f"[{status_emoji} {status_desc}]", status_color)
                
                for key, value in config.items():
                    # Skip displaying status fields in the config details
                    if key in ['node_status', 'last_status_update']:
                        continue
                    if any(k in key.lower() for k in ["password", "key"]):
                        value = "********"
                    self.print_colored(f"  {key}: {value}")

        input("\nPress Enter to continue...")

    def test_connectivity(self) -> None:
        """Test connectivity to configured nodes"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Testing Node Connectivity")

        # Load configuration to show nodes being tested
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        # Show pre-test information
        self.print_colored(f"🔧 Connectivity Test Details:", 'cyan', bold=True)
        self.print_colored(f"   • Network: {env if env else 'Not set'}", 'green' if env else 'red')
        self.print_colored(f"   • Nodes to test: {len(hosts)}", 'white')

        self.print_colored(f"\n🖥️  Testing connectivity to:", 'cyan', bold=True)
        for name, config in hosts.items():
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            auth_type = "Password" if 'ansible_ssh_pass' in config else "SSH Key"
            self.print_colored(f"   • {name}: {user}@{ip} ({auth_type})", 'white')

        playbook_path = self.config_dir / 'playbooks/test_connection.yml'
        if not playbook_path.exists():
            self.print_colored(f"❌ Test playbook not found: {playbook_path}", 'red')
            input("Press Enter to continue...")
            return

        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} {playbook_path}")

        self.print_colored(f"\n🔍 Running connectivity test...", 'yellow')
        success, output = self.run_command(cmd, show_output=False)

        # Parse the results
        connectivity_results = self._parse_connectivity_output(output)
        
        # Display formatted results
        self._display_connectivity_results(connectivity_results, hosts)

        input("\nPress Enter to continue...")

    def deploy_full(self) -> None:
        """Deploy full setup (Docker + NVIDIA + GPU)"""
        self._deploy_setup("site.yml", "Full Deployment", "Docker + NVIDIA drivers + GPU setup")

    def deploy_docker_only(self) -> None:
        """Deploy Docker only"""
        self._deploy_setup("site.yml", "Docker-Only Deployment", "Docker without GPU setup", extra_vars="skip_gpu=true")

    def delete_edge_node(self) -> None:
        """Delete deployed Edge Node with host selection"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Delete Edge Node Deployment")

        # Load configuration to show target hosts
        self.load_configuration()
        all_hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        # Interactive host selection for deletion
        selected_host_names = self._interactive_host_selection(all_hosts, "Edge Node deletion")
        
        if not selected_host_names:
            self.print_colored("Deletion cancelled - no hosts selected.", 'yellow')
            input("Press Enter to continue...")
            return

        # Filter hosts to only include selected ones
        selected_hosts = {name: config for name, config in all_hosts.items() if name in selected_host_names}

        # Show deletion details
        self.print_colored(f"🗑️  Deletion Details:", 'cyan', bold=True)
        self.print_colored(f"   • Network: {env if env else 'Not set'}", 'white')
        self.print_colored(f"   • Selected Nodes: {len(selected_hosts)}/{len(all_hosts)}", 'white')

        self.print_colored(f"\n🖥️  Edge Nodes will be deleted from these selected machines:", 'cyan', bold=True)
        for name in selected_host_names:
            config = all_hosts[name]
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            # Show current status
            status_info = self._get_node_status_info(name)
            status_emoji, _, status_desc = self._get_status_display_info(status_info['status'])
            self.print_colored(f"   • {name}: {user}@{ip} [{status_emoji} {status_desc}]", 'white')

        self.print_colored("\n⚠️  WARNING: This will completely remove the Edge Node deployment including:", 'red', bold=True)
        self.print_colored("   • Systemd service and Docker containers", 'yellow')
        self.print_colored("   • Docker images and application data", 'yellow')
        self.print_colored("   • Created command scripts", 'yellow')
        self.print_colored("   • Docker daemon configuration", 'yellow')

        if self.get_input(f"\n⚠️  Are you sure you want to delete Edge Node deployment from {len(selected_hosts)} selected machine(s) (y/n)", "n").lower() != 'y':
            self.print_colored("Deletion cancelled.", 'yellow')
            return

        # Final confirmation
        if self.get_input(f"⚠️  Type 'DELETE' to confirm deletion from {len(selected_hosts)} selected node(s)", "").upper() != 'DELETE':
            self.print_colored("Deletion cancelled - confirmation not received.", 'yellow')
            return

        playbook_path = self.config_dir / 'playbooks/delete_edge_node.yml'
        if not playbook_path.exists():
            self.print_colored(f"Delete Edge Node playbook not found: {playbook_path}", 'red')
            input("Press Enter to continue...")
            return

        # Create host limit for selected hosts
        host_limit = ','.join(selected_host_names)
        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} --limit {host_limit} {playbook_path}")

        # Update selected node statuses to deploying (deletion is a deployment operation)
        for host_name in selected_host_names:
            self._update_node_status(host_name, 'deploying')

        self.print_colored("\nStarting Edge Node deletion on selected nodes...", 'cyan')
        success, _ = self.run_command(cmd, show_output=True)

        if success:
            self.print_colored("\nEdge Node deleted successfully!", 'green')
            # Update deployment metadata after successful deletion
            self._update_deletion_metadata()
            
            # Update selected node statuses to deleted after successful deletion
            for host_name in selected_host_names:
                self._update_node_status(host_name, 'deleted')
            
            # Show updated statuses
            self.print_colored(f"\n📊 Node Deletion Status:", 'cyan', bold=True)
            for host_name in selected_host_names:
                self.print_colored(f"   • {host_name}: ", 'white', end='')
                self._display_node_status(host_name, compact=True)
                print()  # New line after each status
            
            self.print_colored(f"\n💡 Note: Deleted nodes will maintain their 'deleted' status.", 'cyan')
            self.print_colored("   This status will not change automatically to preserve the deletion history.", 'white')
        else:
            self.print_colored("\nEdge Node deletion encountered issues. Please check the output above.", 'red')
            # Update selected node statuses to error after failed deletion
            for host_name in selected_host_names:
                self._update_node_status(host_name, 'error')
            self.print_colored(f"\n📊 Selected node statuses updated to Error due to deletion failure.", 'yellow')

        input("Press Enter to continue...")



    def _deploy_setup(self, playbook: str, title: str, description: str, extra_vars: str = "") -> None:
        """Common deployment logic with host selection"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header(title)

        # Load configuration to show deployment details
        self.load_configuration()
        all_hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        if not env:
            self.print_colored("\n⚠️  WARNING: Network environment is not set!", 'red', bold=True)
            self.print_colored("   Please set the network environment before deploying.", 'red')
            input("Press Enter to continue...")
            return

        # Interactive host selection with preselection of never-deployed nodes
        selected_host_names = self._interactive_deployment_host_selection(all_hosts, title.lower())
        
        if not selected_host_names:
            self.print_colored("Deployment cancelled - no hosts selected.", 'yellow')
            input("Press Enter to continue...")
            return

        # Filter hosts to only include selected ones
        selected_hosts = {name: config for name, config in all_hosts.items() if name in selected_host_names}

        # Show deployment details
        self.print_colored(f"📋 Deployment Details:", 'cyan', bold=True)
        self.print_colored(f"   • Action: {description}", 'white')
        self.print_colored(f"   • Network: {env}", 'green')
        self.print_colored(f"   • Selected Nodes: {len(selected_hosts)}/{len(all_hosts)}", 'white')

        self.print_colored(f"\n🖥️  Selected Target Machines:", 'cyan', bold=True)
        for name in selected_host_names:
            config = all_hosts[name]
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            # Show status
            status_info = self._get_node_status_info(name)
            status_emoji, _, status_desc = self._get_status_display_info(status_info['status'])
            self.print_colored(f"   • {name}: {user}@{ip} [{status_emoji} {status_desc}]", 'white')

        self.print_colored(f"\n⚠️  This will:", 'yellow', bold=True)
        if "Docker + NVIDIA" in description:
            self.print_colored("   • Install Docker and Docker Compose", 'yellow')
            self.print_colored("   • Install NVIDIA drivers and CUDA toolkit", 'yellow')
            self.print_colored("   • Configure GPU access for containers", 'yellow')
            self.print_colored("   • Deploy and start the Edge Node", 'yellow')
        elif "Docker only" in description:
            self.print_colored("   • Install Docker and Docker Compose", 'yellow')
            self.print_colored("   • Deploy and start the Edge Node (CPU mode)", 'yellow')
        else:
            self.print_colored(f"   • {description}", 'yellow')

        # Check for network change warning for selected hosts
        deployment_status = self.active_config.get('deployment_status', 'never_deployed')
        last_deployed_network = self.active_config.get('last_deployed_network')

        if deployment_status == 'deployed' and last_deployed_network and last_deployed_network != env:
            self.print_colored(f"\n🚨 NETWORK CHANGE DETECTED!", 'red', bold=True)
            self.print_colored(f"   This configuration was previously deployed on: {last_deployed_network}", 'yellow')
            self.print_colored(f"   You are now deploying to: {env}", 'cyan')
            self.print_colored(f"\n⚠️  Important Information:", 'yellow', bold=True)
            self.print_colored(f"   • The Edge Node will run on the NEW network: {env}", 'white')
            self.print_colored(f"   • Selected node addresses will be used:", 'white')
            for name in selected_host_names:
                config = all_hosts[name]
                ip = config.get('ansible_host', 'Unknown')
                self.print_colored(f"     - {name}: {ip}", 'white')
            self.print_colored(f"   • Node configuration and credentials remain the same", 'white')
            self.print_colored(f"   • Only the blockchain network environment changes", 'white')

            if self.get_input(f"\n❓ Are you sure you want to deploy to {env} (different from previous {last_deployed_network})? (y/n)", "n").lower() != 'y':
                self.print_colored("Deployment cancelled due to network change.", 'yellow')
                return

        if self.get_input(f"\n🚀 Continue with deployment to {len(selected_hosts)} selected node(s) on {env}? (y/n)", "y").lower() != 'y':
            self.print_colored("Deployment cancelled.", 'yellow')
            return

        playbook_path = self.config_dir / f'playbooks/{playbook}'
        if not playbook_path.exists():
            self.print_colored(f"Playbook not found: {playbook_path}", 'red')
            input("Press Enter to continue...")
            return

        # Create host limit for selected hosts
        host_limit = ','.join(selected_host_names)
        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} --limit {host_limit} {playbook_path}")

        if extra_vars:
            cmd += f' --extra-vars "{extra_vars}"'

        # Update selected node statuses to deploying
        for host_name in selected_host_names:
            self._update_node_status(host_name, 'deploying')

        self.print_colored(f"\nStarting {title.lower()} on selected nodes...", 'cyan')
        success, _ = self.run_command(cmd, show_output=True)

        if success:
            self.print_colored(f"\n{title} completed successfully!", 'green')
            # Update deployment metadata after successful deployment
            deployment_type = "full" if "NVIDIA" in description else "docker_only"
            self._update_deployment_metadata(deployment_type)
            
            # Update selected node statuses to running after successful deployment
            for host_name in selected_host_names:
                self._update_node_status(host_name, 'running')
            
            # Show updated statuses
            self.print_colored(f"\n📊 Node Deployment Status:", 'cyan', bold=True)
            for host_name in selected_host_names:
                self.print_colored(f"   • {host_name}: ", 'white', end='')
                self._display_node_status(host_name, compact=True)
                print()  # New line after each status
            
            # Display copy-friendly node addresses after successful deployment
            self._display_copy_friendly_addresses(selected_host_names)
        else:
            self.print_colored(f"\n{title} encountered issues. Please check the output above.", 'red')
            # Update selected node statuses to error after failed deployment
            for host_name in selected_host_names:
                self._update_node_status(host_name, 'error')
            self.print_colored(f"\n📊 Selected node statuses updated to Error due to deployment failure.", 'yellow')

        input("Press Enter to continue...")

    def _display_copy_friendly_addresses(self, host_names: List[str]) -> None:
        """Display copy-friendly node addresses after successful deployment"""
        self.print_colored(f"\n🎉 Deployment Complete! Getting node addresses...", 'green', bold=True)
        
        # Get node addresses for the deployed hosts
        playbook_path = self.config_dir / 'playbooks/get_node_info.yml'
        if not playbook_path.exists():
            self.print_colored(f"Node info playbook not found: {playbook_path}", 'red')
            return

        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} {playbook_path}")

        success, output = self.run_command(cmd, show_output=False, timeout=30)
        
        if success:
            node_results = self._parse_node_info_output(output)
            
            # Filter results to only show the successfully deployed nodes
            successful_deployed_nodes = []
            for host_name in host_names:
                if host_name in node_results and node_results[host_name]['status'] == 'success':
                    successful_deployed_nodes.append((host_name, node_results[host_name]))
            
            if successful_deployed_nodes:
                
                self.print_colored(f"\n📋 Your Node Addresses (Ready to Copy!):", 'green', bold=True)
                self.print_colored(f"\n Copy addresses below  and and link them to your licenses in ratio1.ai dashboard.", 'blue')
                
                self.print_colored("=" * 55, 'cyan')
                
                for i, (node_name, result) in enumerate(successful_deployed_nodes, 1):
                    eth_address = result['data'].get('eth_address', 'N/A')
                    self.print_colored(f"{i}. {node_name}", 'yellow')
                    print(f"   {eth_address}")
                    print()
                
                self.print_colored("\n💡 Tip: Double-click to select an address, then Ctrl+Shift+C (Command+Shift+C on Mac) to copy", 'cyan')
                self.print_colored("💡 These addresses will also be available via menu option 7", 'cyan')
            else:
                self.print_colored(f"\n⚠️  Node addresses not ready yet. Use menu option 7 to check again later.", 'yellow')
        else:
            self.print_colored(f"\n⚠️  Could not retrieve node addresses at this time. Use menu option 7 to check later.", 'yellow')

    def _parse_node_info_output(self, output: str) -> Dict[str, Dict[str, Any]]:
        """Parse node info output to extract both successful and failed nodes"""
        node_results = {}
        import re
        
        # First, detect unreachable nodes
        unreachable_pattern = r'fatal: \[([^\]]+)\]: UNREACHABLE!'
        unreachable_matches = re.findall(unreachable_pattern, output)
        
        for node_name in unreachable_matches:
            node_results[node_name] = {
                'status': 'unreachable',
                'data': None
            }
            self.print_debug(f"Found unreachable node: {node_name}")
        
        # Also check for other unreachable patterns
        unreachable_pattern2 = r'unreachable: \[([^\]]+)\]'
        unreachable_matches2 = re.findall(unreachable_pattern2, output)
        
        for node_name in unreachable_matches2:
            if node_name not in node_results:
                node_results[node_name] = {
                    'status': 'unreachable',
                    'data': None
                }
                self.print_debug(f"Found unreachable node (pattern2): {node_name}")
        
        # Now parse successful nodes (existing logic from _get_node_info_data)
        result_blocks = re.split(r'(?=ok: \[[^\]]+\] => \{)', output)
        
        for block in result_blocks:
            if not block.strip():
                continue
                
            # Extract node name from the block
            node_match = re.search(r'ok: \[([^\]]+)\] => \{', block)
            if not node_match:
                continue
                
            node_name = node_match.group(1)
            self.print_debug(f"Processing successful node: {node_name}")
            
            # Check if this block contains node_info.stdout_lines
            if '"node_info.stdout_lines":' not in block:
                self.print_debug(f"No node_info.stdout_lines found for {node_name}")
                continue
            
            try:
                # Extract the JSON lines from stdout_lines array
                stdout_start = block.find('"node_info.stdout_lines":')
                if stdout_start == -1:
                    continue
                
                # Find the opening bracket for the array
                array_start = block.find('[', stdout_start)
                if array_start == -1:
                    continue
                
                # Count brackets to find the matching closing bracket
                bracket_count = 0
                array_end = array_start
                for i in range(array_start, len(block)):
                    if block[i] == '[':
                        bracket_count += 1
                    elif block[i] == ']':
                        bracket_count -= 1
                        if bracket_count == 0:
                            array_end = i
                            break
                
                if bracket_count != 0:
                    continue
                
                # Extract the array content
                array_content = block[array_start+1:array_end]
                
                # Extract all quoted strings from the array content using regex
                # Pattern to match quoted strings, handling escaped quotes
                quoted_pattern = r'"([^"\\]*(?:\\.[^"\\]*)*)"'
                quoted_matches = re.findall(quoted_pattern, array_content)
                
                # Reconstruct the JSON string
                json_str = ''
                for line in quoted_matches:
                    # Unescape the quotes and add to JSON string
                    unescaped_line = line.replace('\\"', '"')
                    json_str += unescaped_line + '\n'
                
                # Parse the JSON
                if json_str.strip():
                    node_data = json.loads(json_str.strip())
                    node_results[node_name] = {
                        'status': 'success',
                        'data': node_data
                    }
                    self.print_debug(f"Successfully parsed JSON for {node_name}")
                    
            except (json.JSONDecodeError, Exception) as e:
                self.print_debug(f"Failed to parse JSON for {node_name}: {e}")
                # If parsing fails, mark as error but don't overwrite unreachable status
                if node_name not in node_results:
                    node_results[node_name] = {
                        'status': 'error',
                        'data': None
                    }
        
        return node_results


    def _parse_node_info_line_by_line(self, output: str) -> Dict[str, Dict[str, Any]]:
        """Fallback method to parse node info line by line"""
        node_info = {}
        lines = output.split('\n')
        
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            
            # Look for node output blocks
            if line.startswith('ok: [') and '] => {' in line:
                # Extract node name
                start = line.find('[') + 1
                end = line.find(']')
                if start > 0 and end > start:
                    node_name = line[start:end]
                    self.print_debug(f"Line-by-line parsing for node: {node_name}")
                    
                    # Find the JSON block for this node
                    json_lines = []
                    i += 1
                    
                    # Look for the start of node_info.stdout_lines
                    while i < len(lines) and 'node_info.stdout_lines' not in lines[i]:
                        i += 1
                    
                    if i < len(lines):
                        self.print_debug(f"Found stdout_lines at line {i}")
                        i += 1  # Skip the stdout_lines line
                        
                        # Collect JSON lines until we hit the end
                        json_started = False
                        while i < len(lines):
                            current_line = lines[i].strip()
                            
                            # Stop if we hit the end of the array or next node
                            if current_line == ']' or current_line == '}' or current_line.startswith('ok: ['):
                                break
                            
                            # Skip empty lines and array markers
                            if not current_line or current_line == '[':
                                i += 1
                                continue
                            
                            # Clean up the JSON line - handle various formats
                            if current_line.startswith('"') and (current_line.endswith('",') or current_line.endswith('"')):
                                # Remove quotes and trailing comma
                                clean_line = current_line[1:]
                                if clean_line.endswith('",'):
                                    clean_line = clean_line[:-2]
                                elif clean_line.endswith('"'):
                                    clean_line = clean_line[:-1]
                                
                                # Unescape quotes
                                clean_line = clean_line.replace('\\"', '"')
                                json_lines.append(clean_line)
                                json_started = True
                            
                            i += 1
                        
                        # Try to parse the collected JSON
                        if json_lines:
                            try:
                                json_str = '\n'.join(json_lines)
                                self.print_debug(f"Line-by-line JSON for {node_name}: {json_str[:200]}...")
                                node_data = json.loads(json_str)
                                node_info[node_name] = node_data
                                self.print_debug(f"Line-by-line parsing successful for {node_name}")
                            except json.JSONDecodeError as e:
                                self.print_debug(f"Line-by-line JSON parsing failed for {node_name}: {e}")
                        else:
                            self.print_debug(f"No JSON lines collected for {node_name}")
            else:
                i += 1
        
        self.print_debug(f"Line-by-line parsing result: {len(node_info)} nodes found")
        return node_info

    def get_node_addresses(self) -> None:
        """Get and display node addresses from actual node information"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Node Addresses")

        playbook_path = self.config_dir / 'playbooks/get_node_info.yml'
        if not playbook_path.exists():
            self.print_colored(f"Node info playbook not found: {playbook_path}", 'red')
            input("Press Enter to continue...")
            return

        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} {playbook_path}")

        self.print_colored("Retrieving node addresses...", 'yellow')

        success, output = self.run_command(cmd, show_output=False, timeout=60)

        # Check for timeout specifically
        if not success and "timed out" in output.lower():
            self.print_colored("Node address retrieval timed out after 60 seconds", 'red')
            self.print_colored("Some nodes may be offline or not responding", 'yellow')
            input("Press Enter to continue...")
            return

        # Parse the output to extract both successful and failed nodes
        # Note: We parse even if success=False because some nodes might have succeeded
        node_results = self._parse_node_info_output(output)
        
        if not node_results:
            self.print_colored("No node information could be parsed from the playbook output.", 'yellow')
            self.print_colored("This might be because the nodes are not running or not accessible.", 'yellow')
        else:
            # Display the addresses in a formatted table
            self.print_colored(f"\nNode Addresses Summary:", 'green')
            self.print_colored(f"{'Node Name':<20} {'ETH Address':<48} {'Status':<15}", 'cyan')
            self.print_colored("-" * 83, 'cyan')
            
            success_count = 0
            unreachable_count = 0
            
            for node_name, result in node_results.items():
                if result['status'] == 'success':
                    address = result['data'].get('address', 'N/A')
                    eth_address = result['data'].get('eth_address', 'N/A')
                    self.print_colored(f"{node_name:<20} {eth_address:<48} {'SUCCESS':<15}", 'green')
                    success_count += 1
                else:
                    self.print_colored(f"{node_name:<20} {'N/A':<48} {'UNREACHABLE':<15}", 'red')
                    unreachable_count += 1
            
            # Show summary
            if success_count > 0 and unreachable_count > 0:
                self.print_colored(f"\n✅ {success_count} node(s) retrieved successfully, ❌ {unreachable_count} node(s) unreachable", 'yellow')
            elif success_count > 0:
                self.print_colored(f"\n✅ All {success_count} node(s) retrieved successfully", 'green')
            else:
                self.print_colored(f"\n❌ All {unreachable_count} node(s) are unreachable", 'red')

        input("\nPress Enter to continue...")

    def export_addresses_csv(self) -> None:
        """Export node addresses to CSV"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Export Addresses to CSV")

        playbook_path = self.config_dir / 'playbooks/get_node_info.yml'
        if not playbook_path.exists():
            self.print_colored(f"Node info playbook not found: {playbook_path}", 'red')
            input("Press Enter to continue...")
            return

        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} {playbook_path}")

        self.print_colored("Retrieving node information for CSV export...", 'yellow')

        success, output = self.run_command(cmd, show_output=False, timeout=60)

        # Check for timeout specifically
        if not success and "timed out" in output.lower():
            self.print_colored("Node info retrieval timed out after 60 seconds", 'red')
            self.print_colored("Some nodes may be offline or not responding", 'yellow')
            input("Press Enter to continue...")
            return

        # Parse the output to extract both successful and failed nodes
        # Note: We parse even if success=False because some nodes might have succeeded
        node_results = self._parse_node_info_output(output)
        
        if not node_results:
            self.print_colored("No node information could be parsed from the playbook output.", 'yellow')
            self.print_colored("This might be because the nodes are not running or not accessible.", 'yellow')
            input("Press Enter to continue...")
            return

        # Count successful and failed nodes
        success_count = sum(1 for result in node_results.values() if result['status'] == 'success')
        unreachable_count = sum(1 for result in node_results.values() if result['status'] == 'unreachable')
        
        self.print_colored(f"Found information for {len(node_results)} node(s) (✅ {success_count} successful, ❌ {unreachable_count} unreachable)", 'green')
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        csv_file = f"node_addresses_{timestamp}.csv"

        try:
            with open(csv_file, 'w') as f:
                f.write("Node_Name,Status,ETH_Address,SSH_Host,SSH_User,Address\n")
                
                # Load configuration to get SSH details
                self.load_configuration()
                hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
                
                for node_name, result in node_results.items():
                    # Get SSH info from configuration
                    ssh_host = 'N/A'
                    ssh_user = 'N/A'
                    if node_name in hosts:
                        ssh_host = hosts[node_name].get('ansible_host', 'N/A')
                        ssh_user = hosts[node_name].get('ansible_user', 'N/A')
                    
                    if result['status'] == 'success':
                        info = result['data']
                        alias = info.get('alias', node_name)
                        address = info.get('address', 'N/A')
                        eth_address = info.get('eth_address', 'N/A')
                        f.write(f'"{node_name}","SUCCESS","{eth_address}","{ssh_host}","{ssh_user}","{address}"\n')
                    else:
                        f.write(f'"{node_name}","UNREACHABLE","N/A","{ssh_host}","{ssh_user}","N/A"\n')

            self.print_colored(f"Addresses exported to: {csv_file}", 'green')
            self.print_colored(f"CSV contains: Node Name, Status, ETH Address, SSH Host, SSH User, Address", 'cyan')
            if success_count > 0 and unreachable_count > 0:
                self.print_colored(f"Note: {success_count} nodes have valid addresses, {unreachable_count} nodes are marked as unreachable", 'yellow')
        except Exception as e:
            self.print_colored(f"Error exporting to CSV: {e}", 'red')

        input("Press Enter to continue...")

    def change_network_environment(self) -> None:
        """Change the network environment"""
        self.print_header("Change Network Environment")

        current_env = self.get_mnl_app_env()
        if current_env:
            self.print_colored(f"Current environment: {current_env}", 'yellow')

        env = self._select_network_environment()
        self.set_mnl_app_env(env)
        self.print_colored(f"Network environment changed to: {env}", 'green')

        input("Press Enter to continue...")

    def check_and_update_node_status(self) -> None:
        """Check actual service status and update node statuses accordingly"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Check & Update Node Status")

        # Load configuration to show current statuses
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})

        # Show current statuses
        self.print_colored(f"🔍 Current Node Statuses:", 'cyan', bold=True)
        self.print_debug(f"Debug: Checking status for {len(hosts)} nodes")
        for name in hosts.keys():
            status_info = self._get_node_status_info(name)
            status = status_info['status']
            last_update = status_info['last_update']
            emoji, color, description = self._get_status_display_info(status)
            last_update_str = self._format_timestamp_ago(last_update)
            self.print_colored(f"   • {name}: {emoji} {description}", color, end='')
            self.print_colored(f" (Last updated: {last_update_str})", 'white')
            self.print_debug(f"  {name}: status={status}, last_update={last_update}")

        self.print_colored(f"\n📋 This will:", 'yellow', bold=True)
        self.print_colored("   • Check the actual service status on all nodes", 'yellow')
        self.print_colored("   • Update node statuses based on real service state", 'yellow')
        self.print_colored("   • Preserve 'Pending Restart' status (requires manual restart)", 'yellow')
        self.print_colored("   • Show differences between tracked and actual status", 'yellow')

        if self.get_input(f"\n🔍 Continue with status check on {len(hosts)} node(s)? (y/n)", "y").lower() != 'y':
            self.print_colored("Status check cancelled.", 'yellow')
            return

        playbook_path = self.config_dir / 'playbooks/service_status.yml'
        if not playbook_path.exists():
            self.print_colored(f"Service status playbook not found: {playbook_path}", 'red')
            input("Press Enter to continue...")
            return

        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} {playbook_path}")

        self.print_colored("\n🔍 Checking actual service status on all nodes...", 'cyan')
        self.print_debug(f"Running ansible command: {cmd}")
        success, output = self.run_command(cmd, show_output=False)

        if success:
            self.print_colored("✅ Service status check completed!", 'green')
            self.print_debug(f"Ansible output length: {len(output)} characters")
            if DEBUG and output:
                self.print_debug("Raw ansible output (first 1000 chars):")
                self.print_debug(output[:1000] + ("..." if len(output) > 1000 else ""))
            
            # Parse the output to determine actual service states
            actual_statuses = self._parse_service_status_output(output)
            
            # Update node statuses based on results
            status_changes = []
            self.print_debug(f"Processing status updates for {len(actual_statuses)} nodes")
            
            # Update last_update timestamp for all nodes that were checked
            for node_name in actual_statuses.keys():
                self._update_node_status(node_name, actual_statuses[node_name])
            
            for node_name, actual_status in actual_statuses.items():
                current_status_info = self._get_node_status_info(node_name)
                current_status = current_status_info['status']
                
                # Determine new status based on actual service state and current tracked status
                new_status = self._determine_updated_status(current_status, actual_status)
                
                self.print_debug(f"Node {node_name}: current={current_status}, actual={actual_status}, new={new_status}")
                
                if new_status != current_status:
                    self._update_node_status(node_name, new_status)
                    status_changes.append({
                        'node': node_name,
                        'old': current_status,
                        'new': new_status,
                        'actual': actual_status
                    })
                    self.print_debug(f"  Status change recorded for {node_name}")
                else:
                    self.print_debug(f"  No status change needed for {node_name}")
            
            # Show results
            self.print_colored(f"\n📊 Status Update Results:", 'cyan', bold=True)
            
            if status_changes:
                self.print_colored(f"Updated {len(status_changes)} node(s):", 'green')
                for change in status_changes:
                    old_emoji, old_color, old_desc = self._get_status_display_info(change['old'])
                    new_emoji, new_color, new_desc = self._get_status_display_info(change['new'])
                    self.print_colored(f"   • {change['node']}: ", 'white', end='')
                    self.print_colored(f"{old_emoji} {old_desc}", old_color, end='')
                    self.print_colored(" → ", 'white', end='')
                    self.print_colored(f"{new_emoji} {new_desc}", new_color)
                    if change['actual'] == 'service_missing':
                        self.print_colored(f"     (Service not found - node may not be deployed)", 'yellow')
                    elif change['actual'] == 'connection_failed':
                        self.print_colored(f"     (Connection failed - node may be unreachable)", 'yellow')
            else:
                self.print_colored("No status changes needed - all nodes are correctly tracked.", 'green')
            
            # Show final status summary
            self.print_colored(f"\n🖥️  Final Node Statuses:", 'cyan', bold=True)
            for name in hosts.keys():
                status_info = self._get_node_status_info(name)
                status = status_info['status']
                last_update = status_info['last_update']
                emoji, color, description = self._get_status_display_info(status)
                last_update_str = self._format_timestamp_ago(last_update)
                self.print_colored(f"   • {name}: {emoji} {description}", color, end='')
                self.print_colored(f" (Last updated: {last_update_str})", 'white')
                
        else:
            self.print_colored("❌ Service status check failed. Please check network connectivity and node access.", 'red')
            self.print_colored("Output for debugging:", 'yellow')
            if output:
                print(output[:500] + "..." if len(output) > 500 else output)

        input("\nPress Enter to continue...")

    def _parse_service_status_output(self, output: str) -> Dict[str, str]:
        """Parse ansible service status output to determine actual service states"""
        node_statuses = {}
        
        try:
            lines = output.split('\n')
            current_node = None
            self.print_debug(f"Parsing ansible output with {len(lines)} lines")
            
            for i, line in enumerate(lines):
                line = line.strip()
                
                # Look for node task execution patterns
                if 'TASK [' in line and 'Check service status' in line:
                    self.print_debug(f"Line {i}: Found task header: {line}")
                    continue
                    
                # Look for node results - patterns like "ok: [node-name]" or "fatal: [node-name]"
                if line.startswith(('ok: [', 'changed: [', 'fatal: [', 'unreachable: [')):
                    # Extract node name
                    start = line.find('[') + 1
                    end = line.find(']')
                    if start > 0 and end > start:
                        current_node = line[start:end]
                        self.print_debug(f"Line {i}: Found node result for {current_node}: {line}")
                        
                        if line.startswith('unreachable:'):
                            node_statuses[current_node] = 'connection_failed'
                            self.print_debug(f"  Set {current_node} to connection_failed")
                        elif line.startswith('fatal:'):
                            # Could be service not found or other error
                            if 'could not be found' in line.lower() or 'not found' in line.lower():
                                node_statuses[current_node] = 'service_missing'
                                self.print_debug(f"  Set {current_node} to service_missing")
                            else:
                                node_statuses[current_node] = 'error'
                                self.print_debug(f"  Set {current_node} to error")
                        continue
                
                # Look for systemctl status output patterns
                # Avoid Ansible summary lines (e.g., "my-node : ok=10 changed=3 failed=0")
                if current_node and not (':' in line and 'ok=' in line and 'changed=' in line):
                    if any(keyword in line.lower() for keyword in ['active', 'inactive', 'failed', 'running']):
                        self.print_debug(f"Line {i}: Status line for {current_node}: {line}")
                        if 'active (running)' in line.lower():
                            node_statuses[current_node] = 'running'
                            self.print_debug(f"  Set {current_node} to running")
                        elif 'inactive' in line.lower() or 'stopped' in line.lower():
                            node_statuses[current_node] = 'stopped'
                            self.print_debug(f"  Set {current_node} to stopped")
                        elif 'failed' in line.lower() and 'failed=' not in line.lower():
                            # Only treat as error if it's not an Ansible stats line like "failed=0"
                            node_statuses[current_node] = 'error'
                            self.print_debug(f"  Set {current_node} to error")
                        
                # Look for service status in structured output (Service Status: ACTIVE)
                elif current_node and 'service status:' in line.lower():
                    self.print_debug(f"Line {i}: Service status line for {current_node}: {line}")
                    if 'active' in line.lower():
                        node_statuses[current_node] = 'running'
                        self.print_debug(f"  Set {current_node} to running (from service status)")
                    elif 'inactive' in line.lower() or 'inactive/failed' in line.lower():
                        node_statuses[current_node] = 'stopped'
                        self.print_debug(f"  Set {current_node} to stopped (from service status)")
                        
            # If we couldn't parse specific statuses, try a broader approach
            if not node_statuses:
                self.print_debug("No statuses parsed, trying broader approach")
                # Look for any node mentions and assume unknown status
                import re
                node_pattern = r'(?:ok|changed|fatal|unreachable): \[([^\]]+)\]'
                matches = re.findall(node_pattern, output)
                self.print_debug(f"Found node mentions: {matches}")
                for node_name in set(matches):
                    if node_name not in node_statuses:
                        # If we see the node but can't determine status, mark as unknown
                        node_statuses[node_name] = 'unknown'
                        self.print_debug(f"  Set {node_name} to unknown (fallback)")
                        
        except Exception as e:
            self.print_debug(f"Error parsing service status output: {e}")
            
        self.print_debug(f"Final parsed node statuses: {node_statuses}")
        return node_statuses

    def _determine_updated_status(self, current_status: str, actual_status: str) -> str:
        """Determine the new status based on current tracked status and actual service state"""
        
        self.print_debug(f"Determining status: current='{current_status}', actual='{actual_status}'")
        
        # Special handling for pending_restart - preserve it unless service is confirmed stopped
        if current_status == 'pending_restart':
            if actual_status == 'stopped' or actual_status == 'service_missing':
                # If service is actually stopped/missing, we can clear pending_restart
                result = 'stopped' if actual_status == 'stopped' else 'never_deployed'
                self.print_debug(f"  Pending restart cleared: {current_status} → {result}")
                return result
            else:
                # Keep pending_restart status - the service might be running but with old config
                self.print_debug(f"  Pending restart preserved (service state: {actual_status})")
                return 'pending_restart'
        
        # For other statuses, update based on actual service state
        status_mapping = {
            'running': 'running',
            'stopped': 'stopped',
            'error': 'error',
            'service_missing': 'never_deployed',
            'connection_failed': 'error',
            'unknown': 'unknown'
        }
        
        result = status_mapping.get(actual_status, 'unknown')
        self.print_debug(f"  Status mapping: {actual_status} → {result}")
        return result

    def _parse_connectivity_output(self, output: str) -> Dict[str, Dict[str, Any]]:
        """Parse ansible connectivity test output to extract connection results"""
        node_results = {}
        
        try:
            lines = output.split('\n')
            self.print_debug(f"Parsing connectivity output with {len(lines)} lines")
            
            # Look for PLAY RECAP section which contains the summary
            recap_started = False
            for line in lines:
                line = line.strip()
                
                if 'PLAY RECAP' in line:
                    recap_started = True
                    continue
                
                if recap_started and line:
                    # Parse lines like: "node-name : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0"
                    if ':' in line and 'ok=' in line:
                        parts = line.split(':', 1)
                        if len(parts) == 2:
                            node_name = parts[0].strip()
                            stats = parts[1].strip()
                            
                            # Parse the statistics
                            stats_dict = {}
                            for stat_pair in stats.split():
                                if '=' in stat_pair:
                                    key, value = stat_pair.split('=', 1)
                                    try:
                                        stats_dict[key] = int(value)
                                    except ValueError:
                                        stats_dict[key] = value
                            
                            # Determine connection status
                            if stats_dict.get('unreachable', 0) > 0:
                                status = 'unreachable'
                                message = 'Connection failed - node is unreachable'
                            elif stats_dict.get('failed', 0) > 0:
                                status = 'failed'
                                message = 'Connection failed - authentication or other error'
                            elif stats_dict.get('ok', 0) > 0:
                                status = 'connected'
                                message = 'Connection successful'
                            else:
                                status = 'unknown'
                                message = 'Unknown connection status'
                            
                            node_results[node_name] = {
                                'status': status,
                                'message': message,
                                'stats': stats_dict
                            }
                            
                            self.print_debug(f"Parsed {node_name}: {status} - {message}")
                            
        except Exception as e:
            self.print_debug(f"Error parsing connectivity output: {e}")
            
        self.print_debug(f"Final connectivity results: {node_results}")
        return node_results

    def _display_connectivity_results(self, connectivity_results: Dict[str, Dict[str, Any]], hosts: Dict[str, Dict[str, Any]]) -> None:
        """Display formatted connectivity test results"""
        
        if not connectivity_results:
            self.print_colored("\n❌ No connectivity results could be parsed from the test output.", 'red')
            self.print_colored("   This might indicate a configuration or network issue.", 'yellow')
            return
        
        # Count results by status
        connected_count = sum(1 for result in connectivity_results.values() if result['status'] == 'connected')
        unreachable_count = sum(1 for result in connectivity_results.values() if result['status'] == 'unreachable')
        failed_count = sum(1 for result in connectivity_results.values() if result['status'] == 'failed')
        total_count = len(connectivity_results)
        
        # Display overall summary
        self.print_colored(f"\n📊 Connectivity Test Results", 'cyan', bold=True)
        self.print_colored(f"   • Total nodes tested: {total_count}", 'white')
        self.print_colored(f"   • Connected: {connected_count}", 'green' if connected_count > 0 else 'white')
        self.print_colored(f"   • Unreachable: {unreachable_count}", 'red' if unreachable_count > 0 else 'white')
        self.print_colored(f"   • Failed: {failed_count}", 'red' if failed_count > 0 else 'white')
        
        # Display detailed results for each node
        self.print_colored(f"\n🔍 Detailed Node Results:", 'cyan', bold=True)
        
        for node_name, result in connectivity_results.items():
            status = result['status']
            message = result['message']
            stats = result.get('stats', {})
            
            # Get node configuration details
            node_config = hosts.get(node_name, {})
            ip = node_config.get('ansible_host', 'Unknown')
            user = node_config.get('ansible_user', 'Unknown')
            
            # Choose appropriate emoji and color
            if status == 'connected':
                emoji = '✅'
                color = 'green'
            elif status == 'unreachable':
                emoji = '🔴'
                color = 'red'
            elif status == 'failed':
                emoji = '❌'
                color = 'red'
            else:
                emoji = '❓'
                color = 'yellow'
            
            # Display node result
            self.print_colored(f"\n   {emoji} {node_name} ({user}@{ip})", color, bold=True)
            self.print_colored(f"      Status: {message}", color)
            
            # Show statistics if available
            if stats:
                ok_count = stats.get('ok', 0)
                changed_count = stats.get('changed', 0)
                unreachable_count = stats.get('unreachable', 0)
                failed_count = stats.get('failed', 0)
                
                self.print_colored(f"      Tasks: {ok_count} successful, {changed_count} changed, {unreachable_count} unreachable, {failed_count} failed", 'white')
        
        # Overall result and recommendations
        if connected_count == total_count:
            self.print_colored(f"\n✅ All {total_count} node(s) are reachable and ready for deployment!", 'green', bold=True)
        elif connected_count > 0:
            self.print_colored(f"\n⚠️  {connected_count} of {total_count} node(s) are reachable", 'yellow', bold=True)
            self.print_colored("   Some nodes have connectivity issues that need to be resolved.", 'yellow')
        else:
            self.print_colored(f"\n❌ All {total_count} node(s) are unreachable", 'red', bold=True)
            self.print_colored("   Please check your network configuration and node settings.", 'red')
        
        # Show troubleshooting tips if there are issues
        if unreachable_count > 0 or failed_count > 0:
            self.print_colored(f"\n💡 Troubleshooting Tips:", 'cyan', bold=True)
            self.print_colored("   • Verify network connectivity (ping the IP addresses)", 'white')
            self.print_colored("   • Check SSH credentials and authentication method", 'white')
            self.print_colored("   • Ensure SSH service is running on target nodes", 'white')
            self.print_colored("   • Verify firewall settings allow SSH connections", 'white')
            self.print_colored("   • Use option 1 → 3 to view and verify your configuration", 'white')
            self.print_colored("   • Use option 10 → 2 to test SSH connection manually", 'white')

    def ssh_into_node_machine(self) -> None:
        """SSH into a selected node's machine"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("SSH Into Node's Machine")

        # Load configuration
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        # Show SSH connection details
        self.print_colored(f"🔧 SSH Connection Details:", 'cyan', bold=True)
        self.print_colored(f"   • Network: {env if env else 'Not set'}", 'green' if env else 'red')
        self.print_colored(f"   • Available Nodes: {len(hosts)}", 'white')

        self.print_colored(f"\n🖥️  Available Machines:", 'cyan', bold=True)
        for name, config in hosts.items():
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            auth_type = "Password" if 'ansible_ssh_pass' in config else "SSH Key"
            status_info = self._get_node_status_info(name)
            status = status_info['status']
            last_update = status_info['last_update']
            emoji, color, description = self._get_status_display_info(status)
            last_update_str = self._format_timestamp_ago(last_update)
            self.print_colored(f"   • {name}: {user}@{ip} ", 'white', end='')
            self.print_colored(f"[{emoji} {description}]", color, end='')
            self.print_colored(f" ({auth_type}) (Last updated: {last_update_str})", 'white')

        self.print_colored(f"\n📋 This will:", 'yellow', bold=True)
        self.print_colored("   • Connect you directly to the selected node via SSH", 'yellow')
        self.print_colored("   • Use the same connection details as configured for deployment", 'yellow')
        self.print_colored("   • Return you to r1setup when you exit the SSH session", 'yellow')
        self.print_colored("   • Type 'exit' in the SSH session to return here", 'yellow')

        # Node selection for SSH - only allow single selection
        print(f"\n🔍 Select a node's machine to SSH into:")
        host_list = list(hosts.keys())
        
        for i, host_name in enumerate(host_list, 1):
            config = hosts[host_name]
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            status_info = self._get_node_status_info(host_name)
            status = status_info['status']
            last_update = status_info['last_update']
            emoji, color, description = self._get_status_display_info(status)
            last_update_str = self._format_timestamp_ago(last_update)
            
            self.print_colored(f"  {i}) {host_name}", 'cyan')
            self.print_colored(f"     └─ {user}@{ip} [{emoji} {description}] (Last updated: {last_update_str})", 'white')
        
        self.print_colored(f"  0) Cancel")

        while True:
            try:
                choice = input(f"\nSelect node (1-{len(host_list)}, 0 to cancel): ").strip()
                
                if choice == '0':
                    self.print_colored("SSH connection cancelled.", 'yellow')
                    return
                
                choice_num = int(choice)
                if 1 <= choice_num <= len(host_list):
                    selected_host = host_list[choice_num - 1]
                    break
                else:
                    self.print_colored(f"Please enter a number between 1 and {len(host_list)}, or 0 to cancel.", 'red')
            except ValueError:
                self.print_colored("Please enter a valid number.", 'red')

        # Get connection details for selected host
        config = hosts[selected_host]
        ip = config.get('ansible_host')
        user = config.get('ansible_user')
        ssh_port = config.get('ansible_port', 22)
        
        if not ip or not user:
            self.print_colored(f"❌ Missing connection details for {selected_host}. Please reconfigure the node.", 'red')
            input("Press Enter to continue...")
            return

        # Build SSH command
        ssh_cmd = ['ssh']
        
        # Add port if not default
        if ssh_port != 22:
            ssh_cmd.extend(['-p', str(ssh_port)])
        
        # Handle SSH key vs password authentication
        if 'ansible_ssh_pass' in config:
            # Password authentication - warn user
            self.print_colored(f"\n⚠️  Password Authentication Required:", 'yellow', bold=True)
            self.print_colored(f"   • This node uses password authentication", 'yellow')
            self.print_colored(f"   • You'll be prompted for the password when connecting", 'yellow')
            password = config.get('ansible_ssh_pass')
            # We can't easily pass password to SSH, so just inform user
            self.print_colored(f"   • Use the same password configured for this node", 'white')
        else:
            # SSH key authentication
            if 'ansible_ssh_private_key_file' in config:
                key_file = config['ansible_ssh_private_key_file']
                # Expand user path if needed
                if key_file.startswith('~'):
                    key_file = os.path.expanduser(key_file)
                ssh_cmd.extend(['-i', key_file])
                self.print_colored(f"\n🔑 Using SSH key: {key_file}", 'green')
            else:
                self.print_colored(f"\n🔑 Using default SSH key authentication", 'green')
        
        # Add any additional SSH options for better connectivity
        ssh_cmd.extend([
            '-o', 'StrictHostKeyChecking=no',  # Don't prompt for host key verification
            '-o', 'UserKnownHostsFile=/dev/null',  # Don't save host keys
            '-o', 'ConnectTimeout=10',  # 10 second connection timeout
        ])
        
        # Add user@host
        ssh_cmd.append(f"{user}@{ip}")

        self.print_colored(f"\n🚀 Connecting to {selected_host} ({user}@{ip})...", 'cyan', bold=True)
        self.print_colored("   Type 'exit' to return to r1setup", 'white')
        self.print_colored("   Press Enter to continue...", 'yellow')
        input()

        try:
            # Execute SSH command
            self.print_colored(f"Executing: {' '.join(ssh_cmd[:4])} ... {ssh_cmd[-1]}", 'cyan')
            
            # Use subprocess.run to execute SSH interactively
            result = subprocess.run(ssh_cmd)
            
            # When SSH exits, we return here
            self.print_colored(f"\n✅ SSH session to {selected_host} ended.", 'green')
            self.print_colored("Returning to r1setup...", 'cyan')
            
        except KeyboardInterrupt:
            self.print_colored(f"\n🛑 SSH connection interrupted.", 'yellow')
        except Exception as e:
            self.print_colored(f"\n❌ SSH connection failed: {e}", 'red')
            self.print_colored("Common issues:", 'yellow')
            self.print_colored("   • Network connectivity problems", 'white')
            self.print_colored("   • Incorrect SSH credentials", 'white')
            self.print_colored("   • Firewall blocking SSH port", 'white')
            self.print_colored("   • Node is not reachable", 'white')
        
        input("\nPress Enter to continue...")

    def start_edge_node_service(self) -> None:
        """Start Edge Node on all configured nodes"""
        self._manage_service("service_start.yml", "Start Edge Node", "🚀 Starting Edge Node")

    def stop_edge_node_service(self) -> None:
        """Stop Edge Node on all configured nodes"""
        self._manage_service("service_stop.yml", "Stop Edge Node", "🛑 Stopping Edge Node")

    def restart_edge_node_service(self) -> None:
        """Restart Edge Node on all configured nodes"""
        self._manage_service("service_restart.yml", "Restart Edge Node", "🔄 Restarting Edge Node")


    def _update_deployment_metadata(self, deployment_type: str) -> None:
        """Update deployment metadata after successful deployment"""
        self.print_debug(f"Updating deployment metadata with type: {deployment_type}")
        self.print_debug(f"Active config: {self.active_config}")
        
        if not self.active_config.get('config_name'):
            self.print_debug("No config_name in active_config, cannot update deployment metadata")
            self.print_colored("Warning: No configuration name found, cannot update deployment tracking", 'yellow')
            return

        config_name = self.active_config['config_name']
        metadata_path = self.configs_dir / f"{config_name}.json"
        self.print_debug(f"Looking for metadata file: {metadata_path}")

        if not metadata_path.exists():
            self.print_debug(f"Metadata file does not exist: {metadata_path}")
            self.print_colored(f"Warning: Metadata file not found: {metadata_path}", 'yellow')
            return

        try:
            # Load existing metadata
            with open(metadata_path) as f:
                metadata = json.load(f)

            self.print_debug(f"Loaded existing metadata: {metadata}")

            # Update deployment info
            current_network = self.get_mnl_app_env()
            metadata['last_deployed_date'] = datetime.now().isoformat()
            metadata['last_deployed_network'] = current_network
            metadata['deployment_status'] = 'deployed'
            metadata['last_deployment_type'] = deployment_type

            self.print_debug(f"Updated metadata: {metadata}")

            # Save updated metadata
            with open(metadata_path, 'w') as f:
                json.dump(metadata, f, indent=2)

            # Update active config
            self.active_config.update(metadata)
            self._save_active_config()

            self.print_colored(f"✅ Deployment tracking updated for configuration: {config_name}", 'green')
            self.print_debug(f"Successfully updated deployment metadata for {config_name}")
        except Exception as e:
            self.print_colored(f"Warning: Could not update deployment metadata: {e}", 'yellow')
            self.print_debug(f"Error updating deployment metadata: {e}")

    def _update_deletion_metadata(self) -> None:
        """Update deployment metadata after successful deletion"""
        if not self.active_config.get('config_name'):
            return

        config_name = self.active_config['config_name']
        metadata_path = self.configs_dir / f"{config_name}.json"

        if not metadata_path.exists():
            return

        try:
            # Load existing metadata
            with open(metadata_path) as f:
                metadata = json.load(f)

            # Update deletion info
            metadata['last_deleted_date'] = datetime.now().isoformat()
            metadata['deployment_status'] = 'deleted'
            # Keep the last deployment info for reference but mark as deleted

            # Save updated metadata
            with open(metadata_path, 'w') as f:
                json.dump(metadata, f, indent=2)

            # Update active config
            self.active_config.update(metadata)
            self._save_active_config()

            self.print_colored(f"✅ Deletion tracking updated for configuration: {config_name}", 'green')
        except Exception as e:
            self.print_colored(f"Warning: Could not update deletion metadata: {e}", 'yellow')

    def _create_ssl_context(self) -> ssl.SSLContext:
        """Create SSL context for secure connections, with certifi support"""
        try:
            # Try to create a default SSL context
            context = ssl.create_default_context()
            
            # Try to use certifi for certificate verification (should be installed in venv)
            try:
                import certifi
                context.load_verify_locations(certifi.where())
                # If we reach here, certifi is working properly
                return context
            except ImportError:
                # Certifi not available, try alternative approaches
                if self.os_type == "macos":
                    try:
                        # Try to load system root certificates on macOS
                        context.load_default_certs()
                        return context
                    except Exception:
                        # If all else fails, disable certificate verification for GitHub
                        # This is safe since we're only downloading from GitHub releases
                        self.print_colored("Warning: Could not load system certificates, disabling SSL verification for updates", 'yellow')
                        context.check_hostname = False
                        context.verify_mode = ssl.CERT_NONE
                else:
                    # On Linux, the default context should work
                    try:
                        context.load_default_certs()
                        return context
                    except Exception:
                        self.print_colored("Warning: Could not load system certificates, disabling SSL verification for updates", 'yellow')
                        context.check_hostname = False
                        context.verify_mode = ssl.CERT_NONE
            
            return context
            
        except Exception as e:
            # Fallback: create unverified context
            self.print_colored(f"SSL context creation failed ({e}), using unverified context for updates", 'yellow')
            context = ssl.create_default_context()
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE
            return context

    def _check_latest_version(self) -> Tuple[Optional[str], Optional[str]]:
        """Check the latest version from GitHub repository"""
        try:
            req = urllib.request.Request(UPDATE_CHECK_URL)
            req.add_header('User-Agent', f'Ratio1-CLI/{CLI_VERSION}')

            # Create SSL context for secure connection
            ssl_context = self._create_ssl_context()

            with urllib.request.urlopen(req, timeout=10, context=ssl_context) as response:
                content = response.read().decode('utf-8')

                # Parse version from ver.py content
                latest_version = None
                for line in content.split('\n'):
                    line = line.strip()
                    if line.startswith('__VER__') and '=' in line:
                        # Extract version from line like: __VER__ = '1.1.6'
                        version_part = line.split('=')[1].strip()
                        # Remove quotes and whitespace
                        latest_version = version_part.strip('\'"')
                        break

                if not latest_version:
                    self.print_colored("Could not parse version from repository", 'red')
                    return None, None

                # Construct download URLs for the latest version
                # First try release assets, fall back to raw content if needed
                download_urls = {
                    'r1setup': f"{DOWNLOAD_BASE_URL}/v{latest_version}/r1setup",
                    'ver.py': f"{DOWNLOAD_BASE_URL}/v{latest_version}/ver.py",
                    'update.py': f"{DOWNLOAD_BASE_URL}/v{latest_version}/update.py"
                }

                # Fallback URLs using raw GitHub content
                fallback_urls = {
                    'r1setup': "https://raw.githubusercontent.com/Ratio1/r1setup/refs/heads/main/mnl_factory/scripts/r1setup",
                    'ver.py': "https://raw.githubusercontent.com/Ratio1/r1setup/refs/heads/main/mnl_factory/scripts/ver.py",
                    'update.py': "https://raw.githubusercontent.com/Ratio1/r1setup/refs/heads/main/mnl_factory/scripts/update.py"
                }

                return latest_version, download_urls, fallback_urls

        except urllib.error.URLError as e:
            error_msg = str(e)
            if "CERTIFICATE_VERIFY_FAILED" in error_msg or "SSL" in error_msg:
                self.print_colored("SSL certificate verification failed.", 'red')
                self.print_colored("This is a common issue on macOS. Possible solutions:", 'yellow')
                self.print_colored("1. Install certificates: /Applications/Python\\ 3.x/Install\\ Certificates.command", 'white')
                self.print_colored("2. Install certifi: pip install certifi", 'white')
                self.print_colored("3. Update macOS and Python to latest versions", 'white')
            else:
                self.print_colored(f"Network error checking for updates: {e}", 'red')
            return None, None, None
        except Exception as e:
            self.print_colored(f"Error checking for updates: {e}", 'red')
            return None, None, None

    def _compare_versions(self, version1: str, version2: str) -> int:
        """Compare two version strings. Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal"""
        def normalize_version(v):
            # Handle pre-release versions by splitting on '-' and taking first part
            v = v.split('-')[0]
            # Split version into parts and convert to integers
            parts = []
            for part in v.split('.'):
                try:
                    parts.append(int(part))
                except ValueError:
                    # If conversion fails, treat as 0
                    parts.append(0)
            return parts

        v1_parts = normalize_version(version1)
        v2_parts = normalize_version(version2)

        # Pad shorter version with zeros
        max_len = max(len(v1_parts), len(v2_parts))
        v1_parts.extend([0] * (max_len - len(v1_parts)))
        v2_parts.extend([0] * (max_len - len(v2_parts)))

        for i in range(max_len):
            if v1_parts[i] > v2_parts[i]:
                return 1
            elif v1_parts[i] < v2_parts[i]:
                return -1

        return 0

    def _perform_update(self, latest_version: str, download_urls: Dict[str, str], fallback_urls: Dict[str, str] = None) -> bool:
        """Download and install the update"""
        try:
            self.print_colored("Downloading update files...", 'yellow')

            # Get current script path and directory
            current_script = Path(sys.argv[0]).resolve()
            script_dir = current_script.parent

            # Create temporary directory for downloads
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_path = Path(temp_dir)
                downloaded_files = {}

                # Download r1setup, ver.py, and update.py
                for filename, url in download_urls.items():
                    self.print_colored(f"Downloading {filename}...", 'yellow')

                    temp_file = temp_path / filename

                    req = urllib.request.Request(url)
                    req.add_header('User-Agent', f'Ratio1-CLI/{CLI_VERSION}')

                    try:
                        # Create SSL context for secure connection
                        ssl_context = self._create_ssl_context()
                        
                        with urllib.request.urlopen(req, timeout=30, context=ssl_context) as response:
                            with open(temp_file, 'wb') as f:
                                shutil.copyfileobj(response, f)

                        # Verify download
                        if not temp_file.exists() or temp_file.stat().st_size == 0:
                            self.print_colored(f"Download failed - {filename} is empty", 'red')
                            if filename == 'r1setup':
                                return False  # r1setup is critical
                            continue  # Other files are optional

                        downloaded_files[filename] = temp_file
                        self.print_colored(f"✅ Downloaded {filename}", 'green')

                    except urllib.error.URLError as e:
                        # Try fallback URL if available
                        if fallback_urls and filename in fallback_urls:
                            self.print_colored(f"Trying fallback URL for {filename}...", 'yellow')
                            try:
                                fallback_req = urllib.request.Request(fallback_urls[filename])
                                fallback_req.add_header('User-Agent', f'Ratio1-CLI/{CLI_VERSION}')

                                with urllib.request.urlopen(fallback_req, timeout=30, context=ssl_context) as response:
                                    with open(temp_file, 'wb') as f:
                                        shutil.copyfileobj(response, f)

                                # Verify fallback download
                                if not temp_file.exists() or temp_file.stat().st_size == 0:
                                    self.print_colored(f"Fallback download failed - {filename} is empty", 'red')
                                    if filename == 'r1setup':
                                        return False  # r1setup is critical
                                    continue  # Other files are optional

                                downloaded_files[filename] = temp_file
                                self.print_colored(f"✅ Downloaded {filename} (fallback)", 'green')
                                continue  # Success with fallback, move to next file

                            except urllib.error.URLError as fallback_error:
                                self.print_colored(f"Fallback also failed for {filename}: {fallback_error}", 'red')

                        if filename == 'r1setup':
                            # r1setup is critical, fail the update
                            self.print_colored(f"Failed to download critical file {filename}: {e}", 'red')
                            return False
                        else:
                            # Other files are optional, continue without them
                            self.print_colored(f"Warning: Could not download {filename}: {e}", 'yellow')
                            continue

                # Install new files
                self.print_colored("Installing new version...", 'yellow')

                # Install r1setup
                if 'r1setup' in downloaded_files:
                    current_stat = current_script.stat()
                    shutil.move(str(downloaded_files['r1setup']), str(current_script))
                    current_script.chmod(current_stat.st_mode)  # Restore executable permissions
                    self.print_colored("✅ Installed new r1setup", 'green')

                # Install ver.py
                if 'ver.py' in downloaded_files:
                    ver_py_path = script_dir / 'ver.py'
                    shutil.move(str(downloaded_files['ver.py']), str(ver_py_path))
                    ver_py_path.chmod(0o644)  # Set readable permissions
                    self.print_colored("✅ Installed new ver.py", 'green')

                # Install update.py
                if 'update.py' in downloaded_files:
                    update_py_path = script_dir / 'update.py'
                    shutil.move(str(downloaded_files['update.py']), str(update_py_path))
                    update_py_path.chmod(0o755)  # Set executable permissions
                    self.print_colored("✅ Installed new update.py", 'green')

                # Verify the new script works
                self.print_colored("Validating installation...", 'yellow')
                result = subprocess.run([str(current_script), '--version'],
                                        capture_output=True, text=True, timeout=5)
                if result.returncode != 0:
                    self.print_colored("Warning: New version validation failed, but files have been updated", 'yellow')

                self.print_colored("✅ Installation completed successfully", 'green')

                # Show information about the update script
                update_py_path = script_dir / 'update.py'
                if update_py_path.exists():
                    self.print_colored(f"\n💡 Update script available at: {update_py_path}", 'cyan')
                    self.print_colored("   You can run 'python update.py --help' for future update options", 'white')

                return True

        except Exception as e:
            self.print_colored(f"Update installation failed: {e}", 'red')
            return False

    def _update_ansible_collection(self) -> bool:
        """Update the Ansible collection to the latest version"""
        try:
            # Use the same collection path as the setup scripts
            ansible_dir = self.ansible_config_root
            collections_path = ansible_dir / 'collections'
            collection_dir = collections_path / 'ansible_collections' / 'ratio1' / 'multi_node_launcher'

            # Ensure collections directory exists
            collections_path.mkdir(parents=True, exist_ok=True)

            # Set environment variables for the subprocess
            env = os.environ.copy()
            env['ANSIBLE_CONFIG'] = str(self.ansible_config_root / 'ansible.cfg')
            env['ANSIBLE_COLLECTIONS_PATH'] = str(collections_path)
            env['ANSIBLE_HOME'] = str(self.ansible_config_root)

            # Get current version if installed
            current_version = None
            if collection_dir.exists():
                galaxy_yml = collection_dir / 'galaxy.yml'
                if galaxy_yml.exists():
                    try:
                        with open(galaxy_yml, 'r') as f:
                            import yaml
                            galaxy_data = yaml.safe_load(f)
                            current_version = galaxy_data.get('version', 'unknown')
                            self.print_colored(f"  Current collection version: {current_version}", 'cyan')
                    except Exception as e:
                        self.print_debug(f"Could not read current version: {e}")

            # The issue with ansible-galaxy is that --upgrade doesn't actually force update
            # So we need to first uninstall and then reinstall to get the latest version
            
            # Method 1: Try to uninstall first, then reinstall (most reliable)
            if collection_dir.exists():
                self.print_colored("  Removing existing collection to force update...", 'yellow')
                try:
                    import shutil
                    # Remove the entire collection directory
                    shutil.rmtree(collection_dir)
                    self.print_colored("  Existing collection removed successfully", 'green')
                except Exception as e:
                    self.print_colored(f"  Warning: Could not remove existing collection: {e}", 'yellow')
                    # Continue anyway - maybe the install will work

            # Now install the latest version
            self.print_colored("  Installing latest collection from Ansible Galaxy...", 'yellow')

            cmd = [
                'ansible-galaxy', 'collection', 'install',
                'ratio1.multi_node_launcher',
                '--collections-path', str(collections_path),
                '--force' 
            ]

            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=300,  # 5 minute timeout
                env=env
            )

            if result.returncode == 0:
                self.print_colored("  Collection installation completed successfully", 'green')
                
                # Get the new version and verify the update
                new_version = None
                if collection_dir.exists():
                    galaxy_yml = collection_dir / 'galaxy.yml'
                    if galaxy_yml.exists():
                        try:
                            with open(galaxy_yml, 'r') as f:
                                import yaml
                                galaxy_data = yaml.safe_load(f)
                                new_version = galaxy_data.get('version', 'unknown')
                                self.print_colored(f"  Updated to collection version: {new_version}", 'cyan')
                                
                                # Show version change if we had a previous version
                                if current_version and current_version != 'unknown' and new_version != current_version:
                                    self.print_colored(f"  Version changed: {current_version} → {new_version}", 'green')
                                elif current_version and current_version != 'unknown' and new_version == current_version:
                                    self.print_colored(f"  Collection was already at the latest version", 'green')
                        except Exception as e:
                            self.print_debug(f"Could not read new version: {e}")

                # Verify the collection exists and is functional
                verification_success = False
                
                # Method 1: Check filesystem directly
                if collection_dir.exists():
                    verification_success = True
                    self.print_colored("  Collection verified via filesystem check", 'cyan')
                
                # Method 2: Check with ansible-galaxy list as backup verification
                if verification_success:
                    try:
                        verify_cmd = [
                            'ansible-galaxy', 'collection', 'list',
                            'ratio1.multi_node_launcher',
                            '--collections-path', str(collections_path)
                        ]

                        verify_result = subprocess.run(
                            verify_cmd,
                            capture_output=True,
                            text=True,
                            timeout=30,
                            env=env
                        )

                        if DEBUG:
                            self.print_debug(f"Verification command output: {verify_result.stdout}")
                            self.print_debug(f"Verification command stderr: {verify_result.stderr}")

                        if verify_result.returncode == 0 and 'ratio1' in verify_result.stdout.lower():
                            self.print_colored("  Collection verified via ansible-galaxy list", 'cyan')
                        else:
                            self.print_colored("  Warning: ansible-galaxy list verification failed, but filesystem check passed", 'yellow')
                    except Exception as e:
                        self.print_debug(f"Verification command failed: {e}")

                return verification_success
            else:
                # Log the error details
                error_msg = result.stderr.strip() if result.stderr else "Unknown error"
                self.print_colored(f"  ansible-galaxy command failed: {error_msg}", 'red')
                
                # Show command output for debugging
                if DEBUG and result.stdout:
                    self.print_debug(f"Command output: {result.stdout}")

                # Check if it's a common issue and provide helpful hints
                if "timeout" in error_msg.lower() or "connection" in error_msg.lower():
                    self.print_colored("  This might be a network connectivity issue. Try again later.", 'yellow')
                elif "permission" in error_msg.lower():
                    self.print_colored("  This might be a permissions issue. Check directory permissions.", 'yellow')
                elif "not found" in error_msg.lower():
                    self.print_colored("  The collection might not exist on Galaxy. Check the collection name.", 'yellow')

                return False

        except subprocess.TimeoutExpired:
            self.print_colored("  Collection update timed out. This might be due to slow network.", 'yellow')
            return False
        except FileNotFoundError:
            self.print_colored("  ansible-galaxy command not found. Please ensure Ansible is installed.", 'yellow')
            return False
        except Exception as e:
            self.print_colored(f"  Unexpected error updating collection: {e}", 'red')
            if DEBUG:
                import traceback
                self.print_debug(f"Full traceback: {traceback.format_exc()}")
            return False

    def _get_current_collection_version(self, force_refresh: bool = False) -> Optional[str]:
        """Get current collection version using multiple smart methods with caching"""
        # Check cache first (unless force refresh)
        if not force_refresh and self._version_cache['collection_version'] is not None:
            import time
            cache_time = self._version_cache.get('collection_check_time', 0)
            current_time = time.time()
            
            # Use cached version if within cache duration
            if current_time - cache_time < self._version_cache['cache_duration']:
                if DEBUG:
                    self.print_debug(f"Using cached collection version: {self._version_cache['collection_version']}")
                return self._version_cache['collection_version']
        
        current_version = None
        
        # Set environment variables for consistency
        env = os.environ.copy()
        env['ANSIBLE_CONFIG'] = str(self.ansible_config_root / 'ansible.cfg')
        env['ANSIBLE_COLLECTIONS_PATH'] = str(self.ansible_config_root / 'collections')
        env['ANSIBLE_HOME'] = str(self.ansible_config_root)
        
        # Method 1: ansible-galaxy collection list with specific collection (most targeted)
        try:
            cmd = [
                'ansible-galaxy', 'collection', 'list', 
                'ratio1.multi_node_launcher',
                '--collections-path', str(self.ansible_config_root / 'collections')
            ]
            
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=15, env=env)
            
            if result.returncode == 0:
                # Parse output more carefully
                for line in result.stdout.split('\n'):
                    line = line.strip()
                    if 'ratio1.multi_node_launcher' in line:
                        parts = line.split()
                        if len(parts) >= 2:
                            current_version = parts[1]
                            if DEBUG:
                                self.print_debug(f"Method 1 (targeted list): Found version {current_version}")
                            return current_version
            elif DEBUG:
                self.print_debug(f"Method 1 failed: {result.stderr}")
        except Exception as e:
            if DEBUG:
                self.print_debug(f"Method 1 error: {e}")

        # Method 2: ansible-galaxy collection list (broad search)
        if not current_version:
            try:
                cmd = [
                    'ansible-galaxy', 'collection', 'list',
                    '--collections-path', str(self.ansible_config_root / 'collections')
                ]
                
                result = subprocess.run(cmd, capture_output=True, text=True, timeout=20, env=env)
                
                if result.returncode == 0:
                    for line in result.stdout.split('\n'):
                        if 'ratio1.multi_node_launcher' in line:
                            parts = line.split()
                            if len(parts) >= 2:
                                current_version = parts[1]
                                if DEBUG:
                                    self.print_debug(f"Method 2 (broad list): Found version {current_version}")
                                break
                elif DEBUG:
                    self.print_debug(f"Method 2 failed: {result.stderr}")
            except Exception as e:
                if DEBUG:
                    self.print_debug(f"Method 2 error: {e}")

        # Method 3: Read galaxy.yml directly (filesystem approach)
        if not current_version:
            try:
                collection_dir = self.ansible_config_root / 'collections' / 'ansible_collections' / 'ratio1' / 'multi_node_launcher'
                galaxy_yml = collection_dir / 'galaxy.yml'
                
                if galaxy_yml.exists():
                    with open(galaxy_yml, 'r') as f:
                        import yaml
                        galaxy_data = yaml.safe_load(f)
                        current_version = galaxy_data.get('version')
                        if current_version and DEBUG:
                            self.print_debug(f"Method 3 (galaxy.yml): Found version {current_version}")
            except Exception as e:
                if DEBUG:
                    self.print_debug(f"Method 3 error: {e}")

        # Method 4: Read MANIFEST.json (alternative metadata file)
        if not current_version:
            try:
                collection_dir = self.ansible_config_root / 'collections' / 'ansible_collections' / 'ratio1' / 'multi_node_launcher'
                manifest_json = collection_dir / 'MANIFEST.json'
                
                if manifest_json.exists():
                    with open(manifest_json, 'r') as f:
                        import json
                        manifest_data = json.load(f)
                        # MANIFEST.json has different structure
                        collection_info = manifest_data.get('collection_info', {})
                        current_version = collection_info.get('version')
                        if current_version and DEBUG:
                            self.print_debug(f"Method 4 (MANIFEST.json): Found version {current_version}")
            except Exception as e:
                if DEBUG:
                    self.print_debug(f"Method 4 error: {e}")

        # Method 5: Test if collection modules are importable (Python approach)
        if not current_version:
            try:
                # Try to use Python to check if collection is available
                python_check = f"""
import sys
sys.path.insert(0, '{self.ansible_config_root / "collections"}')
try:
    from ansible_collections.ratio1.multi_node_launcher import __version__
    print(__version__)
except:
    try:
        import os
        galaxy_path = '{self.ansible_config_root / "collections" / "ansible_collections" / "ratio1" / "multi_node_launcher" / "galaxy.yml"}'
        if os.path.exists(galaxy_path):
            import yaml
            with open(galaxy_path) as f:
                data = yaml.safe_load(f)
                print(data.get('version', ''))
    except:
        pass
"""
                result = subprocess.run([sys.executable, '-c', python_check], 
                                      capture_output=True, text=True, timeout=10)
                
                if result.returncode == 0 and result.stdout.strip():
                    current_version = result.stdout.strip()
                    if DEBUG:
                        self.print_debug(f"Method 5 (Python import): Found version {current_version}")
            except Exception as e:
                if DEBUG:
                    self.print_debug(f"Method 5 error: {e}")

        # Method 6: Use ansible-doc to check collection availability (functional test)
        if not current_version:
            try:
                # Try to get documentation for a known module in the collection
                cmd = [
                    'ansible-doc', '--list', 
                    '--type', 'module',
                    'ratio1.multi_node_launcher'
                ]
                
                result = subprocess.run(cmd, capture_output=True, text=True, timeout=15, env=env)
                
                if result.returncode == 0 and 'ratio1.multi_node_launcher' in result.stdout:
                    # Collection is available, try to get version from filesystem
                    collection_dir = self.ansible_config_root / 'collections' / 'ansible_collections' / 'ratio1' / 'multi_node_launcher'
                    if collection_dir.exists():
                        # If ansible-doc found it, collection exists, try galaxy.yml again
                        galaxy_yml = collection_dir / 'galaxy.yml'
                        if galaxy_yml.exists():
                            with open(galaxy_yml, 'r') as f:
                                import yaml
                                galaxy_data = yaml.safe_load(f)
                                current_version = galaxy_data.get('version', 'detected')
                                if DEBUG:
                                    self.print_debug(f"Method 6 (ansible-doc + filesystem): Found version {current_version}")
            except Exception as e:
                if DEBUG:
                    self.print_debug(f"Method 6 error: {e}")

        # Clean up version string
        if current_version:
            current_version = str(current_version).strip('\'"')
            
        # Cache the result
        import time
        self._version_cache['collection_version'] = current_version
        self._version_cache['collection_check_time'] = time.time()
            
        if DEBUG:
            self.print_debug(f"Final current version result: {current_version}")
            
        return current_version

    def _check_ansible_collection_version(self) -> Tuple[Optional[str], Optional[str], bool]:
        """Check the current Ansible collection version and if updates are available"""
        try:
            # Use the smart method to get current version
            current_version = self._get_current_collection_version()
            latest_version = None

            # Check latest available version from Galaxy API
            try:
                import urllib.request
                import json
                
                # Use the correct Galaxy API v3 endpoint format
                api_url = "https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/index/ratio1/multi_node_launcher/"
                req = urllib.request.Request(api_url)
                req.add_header('User-Agent', f'Ratio1-CLI/{CLI_VERSION}')
                
                # Create SSL context for secure connection
                ssl_context = self._create_ssl_context()
                
                with urllib.request.urlopen(req, timeout=10, context=ssl_context) as response:
                    data = json.loads(response.read().decode('utf-8'))
                    # The API response has highest_version.version structure
                    highest_version = data.get('highest_version', {})
                    latest_version = highest_version.get('version')
                    if DEBUG:
                        self.print_debug(f"Found latest version via Galaxy API v3: {latest_version}")
            except Exception as e:
                if DEBUG:
                    self.print_debug(f"Galaxy API v3 query failed: {e}")
                
                # Fallback: Try the older API format (keep as backup)
                try:
                    # Try the older API format as fallback
                    api_url = "https://galaxy.ansible.com/api/v1/collections/ratio1/multi_node_launcher/"
                    req = urllib.request.Request(api_url)
                    req.add_header('User-Agent', f'Ratio1-CLI/{CLI_VERSION}')
                    
                    with urllib.request.urlopen(req, timeout=10, context=ssl_context) as response:
                        data = json.loads(response.read().decode('utf-8'))
                        # Try different possible fields for v1 API
                        latest_version = (data.get('latest_version') or 
                                        data.get('version') or 
                                        data.get('current_version'))
                        if DEBUG:
                            self.print_debug(f"Found latest version via fallback API v1: {latest_version}")
                except Exception as e2:
                    if DEBUG:
                        self.print_debug(f"Fallback API v1 also failed: {e2}")
                    # If all else fails, we can't determine the latest version
                    latest_version = None

            # Compare versions if we have both
            update_available = False
            if current_version and latest_version:
                try:
                    # Remove any extra quotes or whitespace
                    current_version = current_version.strip('\'"')
                    latest_version = latest_version.strip('\'"')
                    
                    # Simple version comparison
                    current_parts = [int(x) for x in current_version.split('.')]
                    latest_parts = [int(x) for x in latest_version.split('.')]
                    
                    # Pad shorter version with zeros
                    max_len = max(len(current_parts), len(latest_parts))
                    current_parts.extend([0] * (max_len - len(current_parts)))
                    latest_parts.extend([0] * (max_len - len(latest_parts)))
                    
                    # Compare version parts
                    for i in range(max_len):
                        if latest_parts[i] > current_parts[i]:
                            update_available = True
                            break
                        elif latest_parts[i] < current_parts[i]:
                            break  # Current is newer
                    
                    if DEBUG:
                        self.print_debug(f"Version comparison: current={current_version}, latest={latest_version}, update_available={update_available}")
                    
                except (ValueError, IndexError) as e:
                    if DEBUG:
                        self.print_debug(f"Version comparison failed: {e}")
                    # If we can't compare, assume update might be available
                    update_available = True
            elif current_version and not latest_version:
                # We have current but not latest - assume update might be available
                update_available = True
                if DEBUG:
                    self.print_debug(f"Could not determine latest version, assuming update available")
            elif not current_version and latest_version:
                # No current version means collection is not installed
                update_available = True
                if DEBUG:
                    self.print_debug(f"Collection not installed, update available")
            else:
                # Neither version available - can't determine
                update_available = False
                if DEBUG:
                    self.print_debug(f"Could not determine any versions")

            return current_version, latest_version, update_available

        except Exception as e:
            self.print_colored(f"Error checking Ansible collection version: {e}", 'yellow')
            if DEBUG:
                import traceback
                self.print_debug(f"Full traceback: {traceback.format_exc()}")
            return None, None, False

    def _auto_update_check(self) -> None:
        """Automatically check for and install updates on startup"""
        self.print_colored("🔍 Checking for updates...", 'cyan')
        
        # Check CLI version
        latest_cli_version = None
        cli_update_available = False
        cli_download_urls = None
        cli_fallback_urls = None

        try:
            latest_cli_version, cli_download_urls, cli_fallback_urls = self._check_latest_version()
            if latest_cli_version:
                cli_update_available = self._compare_versions(CLI_VERSION, latest_cli_version) < 0
        except Exception as e:
            self.print_colored(f"Could not check for CLI updates: {e}", 'yellow')

        # Check Ansible collection version
        current_collection_version, latest_collection_version, collection_update_available = self._check_ansible_collection_version()

        # Show update status
        if DEBUG:
            self.print_debug(f"Auto-update check: current_collection_version={current_collection_version}, latest_collection_version={latest_collection_version}, collection_update_available={collection_update_available}")

        if cli_update_available:
            self.print_colored(f"🆕 CLI update available: {CLI_VERSION} → {latest_cli_version}", 'green')
        else:
            self.print_colored(f"✅ CLI is up to date ({CLI_VERSION})", 'green')

        if collection_update_available:
            self.print_colored("🆕 Ansible collection update available", 'green')
        else:
            if current_collection_version:
                self.print_colored(f"✅ Ansible collection is up to date ({current_collection_version})", 'green')
            else:
                self.print_colored("✅ Ansible collection is up to date", 'green')

        # Perform auto-updates
        updates_performed = False

        if cli_update_available and latest_cli_version and cli_download_urls:
            self.print_colored(f"\n🚀 Auto-updating CLI to version {latest_cli_version}...", 'cyan', bold=True)
            success = self._perform_update(latest_cli_version, cli_download_urls, cli_fallback_urls)
            
            if success:
                self.print_colored(f"✅ Successfully updated CLI to version {latest_cli_version}!", 'green')
                self.print_colored("Restarting with the new version...", 'cyan')
                # Give user a moment to see the message
                import time
                time.sleep(2)
                os.execv(sys.executable, [sys.executable] + sys.argv)
            else:
                self.print_colored("❌ CLI auto-update failed. Continuing with current version.", 'red')

        if collection_update_available:
            self.print_colored("\n🚀 Auto-updating Ansible collection...", 'cyan', bold=True)
            if current_collection_version:
                self.print_colored(f"  Current version: {current_collection_version}", 'cyan')
            if latest_collection_version:
                self.print_colored(f"  Updating to: {latest_collection_version}", 'cyan')
            
            success = self._update_ansible_collection()
            
            if success:
                self.print_colored("✅ Ansible collection updated successfully!", 'green')
                updates_performed = True
            else:
                self.print_colored("❌ Ansible collection auto-update failed.", 'red')

        if not cli_update_available and not collection_update_available:
            self.print_colored("✅ All components are up to date!", 'green')
        elif updates_performed:
            self.print_colored("\n✅ Auto-update completed successfully!", 'green')
            input("Press Enter to continue...")
        
        # Add a separator before main menu
        print()

    def ensure_active_configuration(self) -> bool:
        """Ensure there's an active configuration before proceeding to main menu"""
        # Check if we have a valid active configuration
        if self.check_hosts_config():
            return True

        self.print_header("Configuration Required")
        self.print_colored("⚠️  No active configuration detected!", 'red', bold=True)

        # Check if we have saved configurations
        configs = self._list_available_configs()

        if configs:
            # If there's only one configuration, automatically select it
            if len(configs) == 1:
                config_name, metadata = configs[0]
                display_name = config_name.replace('.yml', '')

                # Extract custom name for display
                custom_name = display_name
                if '_' in display_name:
                    parts = display_name.split('_')
                    if len(parts) >= 2:
                        # Find where the timestamp starts (8 digits)
                        for idx, part in enumerate(parts):
                            if len(part) == 8 and part.isdigit():
                                custom_name = '_'.join(parts[:idx])
                                break

                env = metadata.get('environment', 'unknown')
                nodes = metadata.get('nodes_count', 0)

                self.print_colored(f"\n📁 Found 1 saved configuration: {custom_name} ({env}, {nodes} node(s))", 'cyan')
                self.print_colored("Automatically activating the only available configuration...", 'yellow')

                if self._load_config_by_name(display_name):
                    self.print_colored(f"✅ Successfully activated configuration: {custom_name}", 'green')
                    self.print_colored("Proceeding to main menu...", 'green')
                    input("Press Enter to continue...")
                    return True
                else:
                    self.print_colored(f"❌ Failed to activate configuration: {custom_name}", 'red')
                    self.print_colored("The configuration file may be corrupted. Please create a new one.", 'red')
                    self._create_new_configuration_with_management()
                    return True

            # Multiple configurations - show selection menu
            self.print_colored(f"\n📁 Found {len(configs)} saved configuration(s):", 'cyan')
            self.print_colored("It looks like you have existing configurations but none are currently active.", 'yellow')
            self.print_colored("This can happen after reinstalling or updating r1setup.", 'yellow')

            # Show available configurations
            for i, (config_name, metadata) in enumerate(configs, 1):
                display_name = config_name.replace('.yml', '')
                env = metadata.get('environment', 'unknown')
                nodes = metadata.get('nodes_count', 0)
                created_at = metadata.get('created_at')
                last_deployed_date = metadata.get('last_deployed_date')
                deployment_status = metadata.get('deployment_status', 'never_deployed')

                # Extract custom name from the config name
                custom_name = display_name
                if '_' in display_name:
                    parts = display_name.split('_')
                    if len(parts) >= 2:
                        # Find where the timestamp starts (8 digits)
                        for idx, part in enumerate(parts):
                            if len(part) == 8 and part.isdigit():
                                custom_name = '_'.join(parts[:idx])
                                break

                # Format creation date
                created_str = "Unknown"
                if created_at:
                    try:
                        if isinstance(created_at, str):
                            created_dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
                            created_str = created_dt.strftime('%Y-%m-%d %H:%M')
                        else:
                            created_str = datetime.fromtimestamp(created_at).strftime('%Y-%m-%d %H:%M')
                    except:
                        created_str = "Unknown"

                # Format deployment status
                deployment_str = ""
                if deployment_status == 'deployed' and last_deployed_date:
                    try:
                        deployed_dt = datetime.fromisoformat(last_deployed_date.replace('Z', '+00:00'))
                        deployed_str = deployed_dt.strftime('%Y-%m-%d %H:%M')
                        deployment_str = f" | 🚀 Last deployed: {deployed_str}"
                    except:
                        deployment_str = " | 🚀 Deployed"
                elif deployment_status == 'deleted':
                    deployment_str = " | 🗑️ Deleted"
                else:
                    deployment_str = " | 📋 Never deployed"

                self.print_colored(f"  {i}. {custom_name}", 'cyan', bold=True)
                self.print_colored(f"     {env} | {nodes} node(s) | Created: {created_str}{deployment_str}", 'white')

            while True:
                self.print_colored("\n🔧 What would you like to do?", 'cyan', bold=True)
                self.print_colored("  1) Select an existing configuration to activate")
                self.print_colored("  2) Create a new configuration")
                self.print_colored("  3) Import configuration from .r1config file")
                print()
                self.print_colored("  0) Exit")

                choice = self.get_input("\nSelect option (0-3)", "1")

                if choice == '0':
                    self.print_colored("Exiting r1setup.", 'yellow')
                    return False
                elif choice == '1':
                    # Let user select from existing configurations
                    while True:
                        try:
                            selection = int(self.get_input(f"Select configuration number (1-{len(configs)})", "1")) - 1
                            if 0 <= selection < len(configs):
                                selected_config = configs[selection][0].replace('.yml', '')
                                break
                            self.print_colored("Invalid selection", 'red')
                        except ValueError:
                            self.print_colored("Please enter a number", 'red')

                    if self._load_config_by_name(selected_config):
                        self.print_colored(f"✅ Successfully activated configuration: {selected_config}", 'green')
                        self.print_colored("You can now access the main menu.", 'green')
                        input("Press Enter to continue...")
                        return True
                    else:
                        self.print_colored(f"❌ Failed to activate configuration: {selected_config}", 'red')
                        self.print_colored("Please try another configuration or create a new one.", 'red')
                        input("Press Enter to continue...")
                        continue
                elif choice == '2':
                    # Create new configuration
                    self._create_new_configuration_with_management()
                    return True
                elif choice == '3':
                    # Import configuration
                    self._import_configuration()
                    # Check if import was successful by seeing if we now have an active config
                    if self.check_hosts_config():
                        self.print_colored("You can now access the main menu.", 'green')
                        input("Press Enter to continue...")
                        return True
                    else:
                        self.print_colored("Import was cancelled or failed. Please try again.", 'yellow')
                        input("Press Enter to continue...")
                        continue
                else:
                    self.print_colored("Invalid option. Please enter 0, 1, 2, or 3.", 'red')
        else:
            # No saved configurations exist
            self.print_colored("\n📝 No configurations found!", 'yellow')
            self.print_colored("You need to create your first configuration to use r1setup.", 'white')
            self.print_colored("A configuration contains your GPU node connection details and network settings.", 'white')

            while True:
                self.print_colored("\n🔧 What would you like to do?", 'cyan', bold=True)
                self.print_colored("  1) Create your first configuration")
                self.print_colored("  2) Import configuration from .r1config file")
                print()
                self.print_colored("  0) Exit")

                choice = self.get_input("\nSelect option (0-2)", "1")

                if choice == '0':
                    self.print_colored("Exiting r1setup.", 'yellow')
                    return False
                elif choice == '1':
                    self._create_new_configuration_with_management()
                    return True
                elif choice == '2':
                    # Import configuration for first-time users
                    self._import_configuration()
                    # Check if import was successful by seeing if we now have an active config
                    if self.check_hosts_config():
                        self.print_colored("Welcome to r1setup! Your configuration has been imported successfully.", 'green')
                        self.print_colored("You can now access the main menu.", 'green')
                        input("Press Enter to continue...")
                        return True
                    else:
                        self.print_colored("Import was cancelled or failed. Please try again.", 'yellow')
                        input("Press Enter to continue...")
                        continue
                else:
                    self.print_colored("Invalid option. Please enter 0, 1, or 2.", 'red')

    def run(self) -> None:
        """Main program loop"""
        # Handle command line arguments
        global DEBUG
        if len(sys.argv) > 1:
            if sys.argv[1] == '--version':
                print(f"r1setup version {CLI_VERSION}")
                sys.exit(0)
            elif sys.argv[1] == '--debug':
                DEBUG = True
                self.print_colored("Debug mode enabled", 'yellow')

        # Check prerequisites
        if not self.check_ansible_installation():
            self.print_colored("Please ensure Ansible and the required collection are installed.", 'red')
            sys.exit(1)

        # Auto-update check - this runs first before everything else
        self._auto_update_check()

        # Ensure we have an active configuration before proceeding
        if not self.ensure_active_configuration():
            sys.exit(0)

        while True:
            try:
                self.show_main_menu()
                choice = self.get_input("Select option (0-11)", "0")

                if choice == '0':
                    self.print_colored("Thank you for using Ratio1 Multi-Node Launcher Setup!", 'green')
                    break
                elif choice == '1':
                    self.configuration_menu()
                elif choice == '2':
                    self.deployment_menu()
                elif choice == '3':
                    self.start_edge_node_service()
                elif choice == '4':
                    self.stop_edge_node_service()
                elif choice == '5':
                    self.restart_edge_node_service()
                elif choice == '6':
                    self.combined_node_status_and_info()
                elif choice == '7':
                    self.get_node_addresses()
                elif choice == '8':
                    self.export_addresses_csv()
                elif choice == '9':
                    self.test_connectivity()
                elif choice == '10':
                    self.advanced_menu()
                else:
                    self.print_colored("Invalid option. Please try again.", 'red')
                    input("Press Enter to continue...")

            except KeyboardInterrupt:
                self.print_colored("\n\nOperation cancelled by user.", 'yellow')
                break
            except Exception as e:
                self.print_colored(f"An error occurred: {e}", 'red')
                input("Press Enter to continue...")

    def switch_environment(self) -> None:
        """Switch network environment (wrapper for change_network_environment)"""
        self.change_network_environment()

    def deployment_status(self) -> None:
        """Show deployment status for all nodes"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        # Reload active config to ensure deployment status is current
        self._load_active_config()
        
        self.print_header("Deployment Status")
        
        # Load configuration
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        
        if not hosts:
            self.print_colored("No nodes configured.", 'yellow')
            input("Press Enter to continue...")
            return
        
        # Show deployment status overview
        deployment_status = self.active_config.get('deployment_status', 'never_deployed')
        last_deployed_date = self.active_config.get('last_deployed_date')
        last_deployed_network = self.active_config.get('last_deployed_network')
        last_deployment_type = self.active_config.get('last_deployment_type')
        last_deleted_date = self.active_config.get('last_deleted_date')
        
        # Overall deployment status
        self.print_section("Overall Deployment Status")
        
        if deployment_status == 'deployed' and last_deployed_date:
            try:
                deployed_dt = datetime.fromisoformat(last_deployed_date.replace('Z', '+00:00'))
                deployed_str = deployed_dt.strftime('%Y-%m-%d %H:%M')
                self.print_colored(f"🚀 Status: Deployed on {deployed_str}", 'green')
                if last_deployed_network:
                    self.print_colored(f"🌐 Network: {last_deployed_network}", 'cyan')
                if last_deployment_type:
                    self.print_colored(f"🔧 Type: {last_deployment_type}", 'cyan')
            except:
                self.print_colored("🚀 Status: Deployed", 'green')
        elif deployment_status == 'deleted' and last_deleted_date:
            try:
                deleted_dt = datetime.fromisoformat(last_deleted_date.replace('Z', '+00:00'))
                deleted_str = deleted_dt.strftime('%Y-%m-%d %H:%M')
                self.print_colored(f"🗑️ Status: Deleted on {deleted_str}", 'red')
            except:
                self.print_colored("🗑️ Status: Deleted", 'red')
        else:
            self.print_colored("📋 Status: Never deployed", 'yellow')
        
        # Individual node status - check in real-time
        self.print_section(f"Node Status Overview ({len(hosts)} nodes)")
        self.print_colored("🔍 Checking current status (max 30s timeout)...", 'cyan')
        
        # Get real-time status for each node
        real_time_status = self._get_real_time_node_status()
        
        print()  # Add a blank line after the checking message
        
        for host_name in hosts.keys():
            if host_name in real_time_status:
                status = real_time_status[host_name]['status']
                result = real_time_status[host_name]['result']
                emoji, color, description = self._get_status_display_info(status)
                
                self.print_colored(f"  • {host_name}: {emoji} {description}", color, end='')
                self.print_colored(f" (Current status)", 'white')
            else:
                self.print_colored(f"  • {host_name}: ❓ Unable to check", 'yellow', end='')
                self.print_colored(f" (Current status)", 'white')
        
        input("\nPress Enter to continue...")

    def get_logs(self) -> None:
        """Stream logs from selected nodes"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Get Node Logs")
        
        # Load configuration
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        
        if not hosts:
            self.print_colored("No nodes configured.", 'yellow')
            input("Press Enter to continue...")
            return
        
        # Show available nodes
        self.print_section(f"Available Nodes ({len(hosts)})")
        host_list = list(hosts.keys())
        
        for i, host_name in enumerate(host_list, 1):
            host_config = hosts[host_name]
            ip = host_config.get('ansible_host', 'Unknown')
            user = host_config.get('ansible_user', 'Unknown')
            
            # Get status information
            status_info = self._get_node_status_info(host_name)
            status = status_info['status']
            emoji, color, description = self._get_status_display_info(status)
            
            self.print_colored(f"  {i}. {host_name} ({user}@{ip}) ", 'white', end='')
            self.print_colored(f"[{emoji} {description}]", color)
        
        print()
        self.print_colored("  0) Return to main menu")
        print()
        
        while True:
            choice = self.get_input("Select a node to view logs (0 to return)", "0")
            
            if choice == '0':
                return
            
            try:
                node_index = int(choice) - 1
                if 0 <= node_index < len(host_list):
                    selected_node = host_list[node_index]
                    self._stream_node_logs(selected_node)
                    break
                else:
                    self.print_colored("Invalid selection. Please try again.", 'red')
            except ValueError:
                self.print_colored("Invalid input. Please enter a number.", 'red')

    def _stream_node_logs(self, node_name: str) -> None:
        """Stream logs from a specific node"""
        self.print_header(f"Streaming Logs - {node_name}")
        
        # Get node connection details first to validate before asking user to proceed
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        node_config = hosts.get(node_name, {})
        
        host = node_config.get('ansible_host', '')
        user = node_config.get('ansible_user', '')
        
        if not host or not user:
            self.print_colored("Error: Node configuration incomplete.", 'red')
            input("Press Enter to continue...")
            return
        
        self.print_colored(f"📡 Ready to stream logs from: {user}@{host}", 'cyan')
        self.print_colored("🔍 This will run 'get_logs -f' on the target machine", 'white')
        self.print_colored("⚠️  Use Ctrl+C to stop streaming and return to menu", 'yellow', bold=True)
        print()
        
        # Ask user to confirm before starting
        input("Press Enter to start streaming logs...")
        print()
        
        try:
            # Construct SSH command to follow logs using the deployed get_logs script
            ssh_cmd = f"ssh {user}@{host} 'get_logs -f'"
            
            # Handle SSH key vs password authentication
            if 'ansible_ssh_pass' in node_config:
                # Use sshpass for password authentication
                ssh_cmd = f"sshpass -p '{node_config['ansible_ssh_pass']}' {ssh_cmd}"
            
            self.print_colored(f"Connecting to {user}@{host}...", 'yellow')
            print("=" * 80)
            
            # Execute the command
            process = subprocess.Popen(ssh_cmd, shell=True, stdout=subprocess.PIPE, 
                                     stderr=subprocess.STDOUT, text=True, 
                                     universal_newlines=True)
            
            # Stream the output
            try:
                for line in iter(process.stdout.readline, ''):
                    if line:
                        print(line.rstrip())
                    else:
                        break
            except KeyboardInterrupt:
                print("\n" + "=" * 80)
                self.print_colored("\n🛑 Log streaming stopped by user.", 'yellow')
                process.terminate()
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()
            
        except Exception as e:
            self.print_colored(f"Error streaming logs: {e}", 'red')
        
        input("\nPress Enter to continue...")

    def write_logs_to_file(self) -> None:
        """Save node logs to a local file"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header("Write Logs to File")
        
        # Load configuration
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        
        if not hosts:
            self.print_colored("No nodes configured.", 'yellow')
            input("Press Enter to continue...")
            return
        
        # Show available nodes
        self.print_section(f"Available Nodes ({len(hosts)})")
        host_list = list(hosts.keys())
        
        for i, host_name in enumerate(host_list, 1):
            host_config = hosts[host_name]
            ip = host_config.get('ansible_host', 'Unknown')
            user = host_config.get('ansible_user', 'Unknown')
            
            # Get status information
            status_info = self._get_node_status_info(host_name)
            status = status_info['status']
            emoji, color, description = self._get_status_display_info(status)
            
            self.print_colored(f"  {i}. {host_name} ({user}@{ip}) ", 'white', end='')
            self.print_colored(f"[{emoji} {description}]", color)
        
        print()
        self.print_colored("  0) Return to main menu")
        print()
        
        while True:
            choice = self.get_input("Select a node to save logs from (0 to return)", "0")
            
            if choice == '0':
                return
            
            try:
                node_index = int(choice) - 1
                if 0 <= node_index < len(host_list):
                    selected_node = host_list[node_index]
                    self._save_node_logs_to_file(selected_node)
                    break
                else:
                    self.print_colored("Invalid selection. Please try again.", 'red')
            except ValueError:
                self.print_colored("Invalid input. Please enter a number.", 'red')

    def _save_node_logs_to_file(self, node_name: str) -> None:
        """Save logs from a specific node to a local file"""
        self.print_header(f"Save Logs to File - {node_name}")
        
        # Get log lines count
        while True:
            try:
                lines_input = self.get_input("How many recent log lines to save (default: 1000)", "1000")
                lines_count = int(lines_input)
                if lines_count <= 0:
                    self.print_colored("Please enter a positive number.", 'red')
                    continue
                break
            except ValueError:
                self.print_colored("Invalid input. Please enter a number.", 'red')
        
        # Generate default filename
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        default_filename = f"{node_name}_logs_{timestamp}.txt"
        
        # Get output filename
        filename = self.get_input(f"Output filename (default: {default_filename})", default_filename)
        
        # Ensure we have a valid filename
        if not filename.strip():
            filename = default_filename
        
        # Add .txt extension if not present
        if not filename.endswith('.txt'):
            filename += '.txt'
        
        try:
            # Get node connection details
            self.load_configuration()
            hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
            node_config = hosts.get(node_name, {})
            
            host = node_config.get('ansible_host', '')
            user = node_config.get('ansible_user', '')
            
            if not host or not user:
                self.print_colored("Error: Node configuration incomplete.", 'red')
                input("Press Enter to continue...")
                return
            
            # Construct SSH command to get logs using the deployed get_logs script
            ssh_cmd = f"ssh {user}@{host} 'get_logs -n {lines_count}'"
            
            # Handle SSH key vs password authentication
            if 'ansible_ssh_pass' in node_config:
                # Use sshpass for password authentication
                ssh_cmd = f"sshpass -p '{node_config['ansible_ssh_pass']}' {ssh_cmd}"
            
            self.print_colored(f"Connecting to {user}@{host} and retrieving {lines_count} log lines...", 'yellow')
            
            # Execute the command
            result = subprocess.run(ssh_cmd, shell=True, capture_output=True, text=True)
            
            if result.returncode == 0:
                # Save logs to file
                with open(filename, 'w') as f:
                    f.write(f"# Edge Node Logs from {node_name} ({user}@{host})\n")
                    f.write(f"# Retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
                    f.write(f"# Last {lines_count} log lines\n")
                    f.write("# " + "="*60 + "\n\n")
                    f.write(result.stdout)
                
                self.print_colored(f"✅ Logs saved to: {filename}", 'green')
                self.print_colored(f"📝 File size: {os.path.getsize(filename)} bytes", 'cyan')
                self.print_colored(f"📅 Lines saved: {lines_count}", 'cyan')
                
            else:
                self.print_colored(f"❌ Error retrieving logs: {result.stderr}", 'red')
                
        except Exception as e:
            self.print_colored(f"Error saving logs: {e}", 'red')
        
        input("\nPress Enter to continue...")

    def _interactive_deployment_host_selection(self, hosts: Dict[str, Dict[str, Any]], operation_name: str) -> List[str]:
        """Interactive host selection for deployments with never-deployed nodes preselected"""
        if not hosts:
            return []

        # Preselect never-deployed nodes
        never_deployed_hosts = set()
        for host_name in hosts.keys():
            status_info = self._get_node_status_info(host_name)
            if status_info['status'] in ['never_deployed', 'deleted']:
                never_deployed_hosts.add(host_name)

        try:
            import sys
            import tty
            import termios
        except ImportError:
            # Fallback to the previous method if termios is not available (Windows)
            return self._fallback_deployment_host_selection(hosts, operation_name, never_deployed_hosts)

        host_list = ["All hosts"] + list(hosts.keys())
        selected_hosts = never_deployed_hosts.copy()  # Start with never-deployed selected
        current_index = 0
        
        def get_key():
            """Get a single keypress from stdin"""
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(sys.stdin.fileno())
                key = sys.stdin.read(1)
                if key == '\x1b':  # ESC sequence
                    key += sys.stdin.read(2)
                return key
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

        def display_menu():
            """Display the selection menu"""
            # Clear screen and move cursor to top
            print("\033[2J\033[H", end="")
            
            # Header
            self.print_header(f"Select Hosts for {operation_name.title()}")
            
            # Instructions
            self.print_colored("🎮 Navigation Controls:", 'cyan', bold=True)
            self.print_colored("   ↑/↓ Arrow keys    - Navigate up/down", 'white')
            self.print_colored("   Space bar        - Toggle selection", 'white')
            self.print_colored("   Enter           - Confirm selection", 'white')
            self.print_colored("   q/Esc           - Cancel operation", 'white')
            print()
            
            # Show preselection info
            if never_deployed_hosts:
                self.print_colored("💡 Pre-selected nodes that were never deployed or deleted:", 'cyan', bold=True)
                for host_name in sorted(never_deployed_hosts):
                    status_info = self._get_node_status_info(host_name)
                    status_emoji, _, status_desc = self._get_status_display_info(status_info['status'])
                    self.print_colored(f"   • {host_name} [{status_emoji} {status_desc}]", 'green')
                print()
            
            # Status
            all_selected = len(selected_hosts) == len(hosts)
            if len(selected_hosts) == 0:
                self.print_colored("⚠️  No hosts selected!", 'red', bold=True)
            elif all_selected:
                self.print_colored(f"✅ All {len(hosts)} hosts selected", 'green', bold=True)
            else:
                self.print_colored(f"📊 Selected: {len(selected_hosts)}/{len(hosts)} hosts", 'cyan', bold=True)
            print()
            
            # Menu items
            for i, item in enumerate(host_list):
                is_current = (i == current_index)
                
                if item == "All hosts":
                    # All hosts option
                    all_selected = len(selected_hosts) == len(hosts)
                    marker = "✓" if all_selected else " "
                    prefix = "→ " if is_current else "  "
                    color = 'yellow' if is_current else ('green' if all_selected else 'white')
                    style = 'bold' if is_current else False
                    
                    self.print_colored(f"{prefix}[{marker}] {item} ({len(hosts)} total)", color, bold=style)
                else:
                    # Individual host
                    host_name = item
                    host_config = hosts[host_name]
                    ip = host_config.get('ansible_host', 'Unknown')
                    user = host_config.get('ansible_user', 'Unknown')
                    
                    # Get status information
                    status_info = self._get_node_status_info(host_name)
                    status = status_info['status']
                    status_emoji, status_color, status_desc = self._get_status_display_info(status)
                    
                    is_selected = host_name in selected_hosts
                    is_preselected = host_name in never_deployed_hosts
                    marker = "✓" if is_selected else " "
                    prefix = "→ " if is_current else "  "
                    
                    if is_current and is_selected:
                        color = 'yellow'
                    elif is_current:
                        color = 'yellow'
                    elif is_selected:
                        color = 'green'
                    else:
                        color = 'white'
                    
                    style = 'bold' if is_current else False
                    

                    preselect_indicator = " (pre-selected)" if is_preselected else ""
                    self.print_colored(f"{prefix}[{marker}] {host_name}{preselect_indicator}", color, bold=style)

            print()
            self.print_colored("─" * 60, 'blue')

        while True:
            display_menu()
            
            try:
                key = get_key()
                
                if key == '\x1b[A':  # Up arrow
                    current_index = (current_index - 1) % len(host_list)
                elif key == '\x1b[B':  # Down arrow
                    current_index = (current_index + 1) % len(host_list)
                elif key == ' ':  # Space bar - toggle selection
                    current_item = host_list[current_index]
                    if current_item == "All hosts":
                        # Toggle all
                        if len(selected_hosts) == len(hosts):
                            selected_hosts = set()
                        else:
                            selected_hosts = set(hosts.keys())
                    else:
                        # Toggle individual host
                        host_name = current_item
                        if host_name in selected_hosts:
                            selected_hosts.remove(host_name)
                        else:
                            selected_hosts.add(host_name)
                elif key == '\r' or key == '\n':  # Enter - confirm
                    if len(selected_hosts) == 0:
                        # Show error and continue
                        print("\033[2J\033[H", end="")
                        self.print_colored("❌ Cannot proceed without selecting any hosts!", 'red', bold=True)
                        self.print_colored("Press any key to continue...", 'yellow')
                        get_key()
                        continue
                    break
                elif key == 'q' or key == '\x1b':  # q or Esc - cancel
                    return []
                
            except KeyboardInterrupt:
                return []
        
        return list(selected_hosts)

    def _interactive_host_selection(self, hosts: Dict[str, Dict[str, Any]], operation_name: str) -> List[str]:
        """Interactive host selection with keyboard navigation (arrow keys + space)"""
        if not hosts:
            return []

        try:
            import sys
            import tty
            import termios
        except ImportError:
            # Fallback to the previous method if termios is not available (Windows)
            return self._fallback_host_selection(hosts, operation_name)

        host_list = ["All hosts"] + list(hosts.keys())
        selected_hosts = set(hosts.keys())  # Start with all selected
        current_index = 0
        
        def get_key():
            """Get a single keypress from stdin"""
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(sys.stdin.fileno())
                key = sys.stdin.read(1)
                if key == '\x1b':  # ESC sequence
                    key += sys.stdin.read(2)
                return key
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

        def display_menu():
            """Display the selection menu"""
            # Clear screen and move cursor to top
            print("\033[2J\033[H", end="")
            
            # Header
            self.print_header(f"Select Hosts for {operation_name.title()}")
            
            # Instructions
            self.print_colored("🎮 Navigation Controls:", 'cyan', bold=True)
            self.print_colored("   ↑/↓ Arrow keys    - Navigate up/down", 'white')
            self.print_colored("   Space bar        - Toggle selection", 'white')
            self.print_colored("   Enter           - Confirm selection", 'white')
            self.print_colored("   q/Esc           - Cancel operation", 'white')
            print()
            
            # Status
            all_selected = len(selected_hosts) == len(hosts)
            if len(selected_hosts) == 0:
                self.print_colored("⚠️  No hosts selected!", 'red', bold=True)
            elif all_selected:
                self.print_colored(f"✅ All {len(hosts)} hosts selected", 'green', bold=True)
            else:
                self.print_colored(f"📊 Selected: {len(selected_hosts)}/{len(hosts)} hosts", 'cyan', bold=True)
            print()
            
            # Menu items
            for i, item in enumerate(host_list):
                is_current = (i == current_index)
                
                if item == "All hosts":
                    # All hosts option
                    all_selected = len(selected_hosts) == len(hosts)
                    marker = "✓" if all_selected else " "
                    prefix = "→ " if is_current else "  "
                    color = 'yellow' if is_current else ('green' if all_selected else 'white')
                    style = 'bold' if is_current else False
                    
                    self.print_colored(f"{prefix}[{marker}] {item} ({len(hosts)} total)", color, bold=style)
                else:
                    # Individual host
                    host_name = item
                    host_config = hosts[host_name]
                    ip = host_config.get('ansible_host', 'Unknown')
                    user = host_config.get('ansible_user', 'Unknown')
                    
                    # Get status information
                    status_info = self._get_node_status_info(host_name)
                    status = status_info['status']
                    status_emoji, status_color, status_desc = self._get_status_display_info(status)
                    
                    is_selected = host_name in selected_hosts
                    marker = "✓" if is_selected else " "
                    prefix = "→ " if is_current else "  "
                    
                    if is_current and is_selected:
                        color = 'yellow'
                    elif is_current:
                        color = 'yellow'
                    elif is_selected:
                        color = 'green'
                    else:
                        color = 'white'
                    
                    style = 'bold' if is_current else False
                    
                    self.print_colored(f"{prefix}[{marker}] {host_name}", color, bold=style)

            print()
            self.print_colored("─" * 60, 'blue')

        while True:
            display_menu()
            
            try:
                key = get_key()
                
                if key == '\x1b[A':  # Up arrow
                    current_index = (current_index - 1) % len(host_list)
                elif key == '\x1b[B':  # Down arrow
                    current_index = (current_index + 1) % len(host_list)
                elif key == ' ':  # Space bar - toggle selection
                    current_item = host_list[current_index]
                    if current_item == "All hosts":
                        # Toggle all
                        if len(selected_hosts) == len(hosts):
                            selected_hosts = set()
                        else:
                            selected_hosts = set(hosts.keys())
                    else:
                        # Toggle individual host
                        host_name = current_item
                        if host_name in selected_hosts:
                            selected_hosts.remove(host_name)
                        else:
                            selected_hosts.add(host_name)
                elif key == '\r' or key == '\n':  # Enter - confirm
                    if len(selected_hosts) == 0:
                        # Show error and continue
                        print("\033[2J\033[H", end="")
                        self.print_colored("❌ Cannot proceed without selecting any hosts!", 'red', bold=True)
                        self.print_colored("Press any key to continue...", 'yellow')
                        get_key()
                        continue
                    break
                elif key == 'q' or key == '\x1b':  # q or Esc - cancel
                    return []
                
            except KeyboardInterrupt:
                return []
        
        return list(selected_hosts)

    def _fallback_deployment_host_selection(self, hosts: Dict[str, Dict[str, Any]], operation_name: str, never_deployed_hosts: set) -> List[str]:
        """Fallback deployment host selection for systems without termios (like Windows)"""
        if not hosts:
            return []

        host_list = list(hosts.keys())
        selected_hosts = never_deployed_hosts.copy()  # Start with never-deployed selected
        
        while True:
            # Clear screen for better UX
            print("\033[2J\033[H", end="")
            
            self.print_header(f"Host Selection for {operation_name.title()}")
            
            if never_deployed_hosts:
                self.print_colored("💡 Pre-selected nodes that were never deployed or deleted:", 'cyan', bold=True)
                for host_name in sorted(never_deployed_hosts):
                    status_info = self._get_node_status_info(host_name)
                    status_emoji, _, status_desc = self._get_status_display_info(status_info['status'])
                    self.print_colored(f"   • {host_name} [{status_emoji} {status_desc}]", 'green')
                print()
            
            self.print_colored("📋 Instructions:", 'cyan', bold=True)
            self.print_colored("   • Enter numbers to toggle selection (e.g., 1, 2, 3)", 'white')
            self.print_colored("   • Use 'a' to select all hosts", 'white')
            self.print_colored("   • Use 'n' to deselect all hosts", 'white')
            self.print_colored("   • Use 'c' to cancel operation", 'white')
            self.print_colored("   • Press Enter when ready to proceed", 'white')
            print()
            
            # Show "all" option
            all_selected = len(selected_hosts) == len(host_list)
            all_marker = "✓" if all_selected else " "
            all_color = 'green' if all_selected else 'white'
            self.print_colored(f"  0) [{all_marker}] All hosts ({len(host_list)} total)", all_color, bold=all_selected)
            
            self.print_colored(f"\n🖥️  Individual Hosts:", 'cyan', bold=True)
            
            # Show individual hosts
            for i, host_name in enumerate(host_list, 1):
                host_config = hosts[host_name]
                ip = host_config.get('ansible_host', 'Unknown')
                user = host_config.get('ansible_user', 'Unknown')
                
                # Get status information
                status_info = self._get_node_status_info(host_name)
                status = status_info['status']
                status_emoji, status_color, status_desc = self._get_status_display_info(status)

                is_selected = host_name in selected_hosts
                is_preselected = host_name in never_deployed_hosts
                marker = "✓" if is_selected else " "
                color = 'green' if is_selected else 'white'
                
                preselect_indicator = " (pre-selected)" if is_preselected else ""
                self.print_colored(f"  {i}) [{marker}] {host_name}{preselect_indicator}", color, bold=is_selected)

            print()
            
            # Status summary
            if len(selected_hosts) == 0:
                self.print_colored("⚠️  No hosts selected!", 'red', bold=True)
                self.print_colored("   Please select at least one host to continue.", 'red')
            elif len(selected_hosts) == len(host_list):
                self.print_colored(f"✅ All {len(host_list)} hosts selected", 'green', bold=True)
            else:
                self.print_colored(f"📊 Selected: {len(selected_hosts)}/{len(host_list)} hosts", 'cyan', bold=True)
                selected_names = ', '.join(sorted(selected_hosts))
                self.print_colored(f"   Selected hosts: {selected_names}", 'cyan')
            
            print()
            self.print_colored("─" * 60, 'blue')
            
            choice = self.get_input("Enter choice (number/a/n/c/Enter to proceed)", "").strip().lower()
            
            if choice == "":
                # Proceed with current selection
                if len(selected_hosts) == 0:
                    self.print_colored("❌ Cannot proceed without selecting any hosts!", 'red')
                    input("Press Enter to continue selection...")
                    continue
                break
            elif choice == "c":
                # Cancel operation
                return []
            elif choice == "a":
                # Select all
                selected_hosts = set(host_list)
                self.print_colored("✅ All hosts selected", 'green')
            elif choice == "n":
                # Select none
                selected_hosts = set()
                self.print_colored("⚠️  All hosts deselected", 'yellow')
            else:
                # Try to parse as number or comma-separated numbers
                try:
                    # Handle multiple numbers separated by commas/spaces
                    choices = []
                    for part in choice.replace(',', ' ').split():
                        try:
                            choices.append(int(part))
                        except ValueError:
                            continue
                    
                    if not choices:
                        # Try single number
                        choices = [int(choice)]
                    
                    # Process each choice
                    for choice_num in choices:
                        if choice_num == 0:
                            # Toggle all
                            if all_selected:
                                selected_hosts = set()
                                self.print_colored("⚠️  All hosts deselected", 'yellow')
                            else:
                                selected_hosts = set(host_list)
                                self.print_colored("✅ All hosts selected", 'green')
                        elif 1 <= choice_num <= len(host_list):
                            # Toggle individual host
                            host_name = host_list[choice_num - 1]
                            if host_name in selected_hosts:
                                selected_hosts.remove(host_name)
                                self.print_colored(f"➖ Deselected: {host_name}", 'yellow')
                            else:
                                selected_hosts.add(host_name)
                                self.print_colored(f"➕ Selected: {host_name}", 'green')
                        else:
                            self.print_colored(f"❌ Invalid choice: {choice_num} (valid range: 0-{len(host_list)})", 'red')
                    
                except ValueError:
                    self.print_colored("❌ Invalid input. Please enter numbers, 'a', 'n', 'c', or press Enter.", 'red')
            
            # Small delay to let user see the feedback
            import time
            time.sleep(0.8)
        
        return list(selected_hosts)

    def _fallback_host_selection(self, hosts: Dict[str, Dict[str, Any]], operation_name: str) -> List[str]:
        """Fallback host selection for systems without termios (like Windows)"""
        if not hosts:
            return []

        host_list = list(hosts.keys())
        selected_hosts = set(host_list)  # Start with all selected
        
        while True:
            # Clear screen for better UX
            print("\033[2J\033[H", end="")
            
            self.print_header(f"Host Selection for {operation_name.title()}")
            
            self.print_colored("📋 Instructions:", 'cyan', bold=True)
            self.print_colored("   • Enter numbers to toggle selection (e.g., 1, 2, 3)", 'white')
            self.print_colored("   • Use 'a' to select all hosts", 'white')
            self.print_colored("   • Use 'n' to deselect all hosts", 'white')
            self.print_colored("   • Use 'c' to cancel operation", 'white')
            self.print_colored("   • Press Enter when ready to proceed", 'white')
            print()
            
            # Show "all" option
            all_selected = len(selected_hosts) == len(host_list)
            all_marker = "✓" if all_selected else " "
            all_color = 'green' if all_selected else 'white'
            self.print_colored(f"  0) [{all_marker}] All hosts ({len(host_list)} total)", all_color, bold=all_selected)
            
            self.print_colored(f"\n🖥️  Individual Hosts:", 'cyan', bold=True)
            
            # Show individual hosts
            for i, host_name in enumerate(host_list, 1):
                host_config = hosts[host_name]
                ip = host_config.get('ansible_host', 'Unknown')
                user = host_config.get('ansible_user', 'Unknown')
                
                # Get status information
                status_info = self._get_node_status_info(host_name)
                status = status_info['status']
                status_emoji, status_color, status_desc = self._get_status_display_info(status)

                is_selected = host_name in selected_hosts
                marker = "✓" if is_selected else " "
                color = 'green' if is_selected else 'white'
                
                self.print_colored(f"  {i}) [{marker}] {host_name}", color, bold=is_selected)

            print()
            
            # Status summary
            if len(selected_hosts) == 0:
                self.print_colored("⚠️  No hosts selected!", 'red', bold=True)
                self.print_colored("   Please select at least one host to continue.", 'red')
            elif len(selected_hosts) == len(host_list):
                self.print_colored(f"✅ All {len(host_list)} hosts selected", 'green', bold=True)
            else:
                self.print_colored(f"📊 Selected: {len(selected_hosts)}/{len(host_list)} hosts", 'cyan', bold=True)
                selected_names = ', '.join(sorted(selected_hosts))
                self.print_colored(f"   Selected hosts: {selected_names}", 'cyan')
            
            print()
            self.print_colored("─" * 60, 'blue')
            
            choice = self.get_input("Enter choice (number/a/n/c/Enter to proceed)", "").strip().lower()
            
            if choice == "":
                # Proceed with current selection
                if len(selected_hosts) == 0:
                    self.print_colored("❌ Cannot proceed without selecting any hosts!", 'red')
                    input("Press Enter to continue selection...")
                    continue
                break
            elif choice == "c":
                # Cancel operation
                return []
            elif choice == "a":
                # Select all
                selected_hosts = set(host_list)
                self.print_colored("✅ All hosts selected", 'green')
            elif choice == "n":
                # Select none
                selected_hosts = set()
                self.print_colored("⚠️  All hosts deselected", 'yellow')
            else:
                # Try to parse as number or comma-separated numbers
                try:
                    # Handle multiple numbers separated by commas/spaces
                    choices = []
                    for part in choice.replace(',', ' ').split():
                        try:
                            choices.append(int(part))
                        except ValueError:
                            continue
                    
                    if not choices:
                        # Try single number
                        choices = [int(choice)]
                    
                    # Process each choice
                    for choice_num in choices:
                        if choice_num == 0:
                            # Toggle all
                            if all_selected:
                                selected_hosts = set()
                                self.print_colored("⚠️  All hosts deselected", 'yellow')
                            else:
                                selected_hosts = set(host_list)
                                self.print_colored("✅ All hosts selected", 'green')
                        elif 1 <= choice_num <= len(host_list):
                            # Toggle individual host
                            host_name = host_list[choice_num - 1]
                            if host_name in selected_hosts:
                                selected_hosts.remove(host_name)
                                self.print_colored(f"➖ Deselected: {host_name}", 'yellow')
                            else:
                                selected_hosts.add(host_name)
                                self.print_colored(f"➕ Selected: {host_name}", 'green')
                        else:
                            self.print_colored(f"❌ Invalid choice: {choice_num} (valid range: 0-{len(host_list)})", 'red')
                    
                except ValueError:
                    self.print_colored("❌ Invalid input. Please enter numbers, 'a', 'n', 'c', or press Enter.", 'red')
            
            # Small delay to let user see the feedback
            import time
            time.sleep(0.8)
        
        return list(selected_hosts)

    def _format_timestamp_ago(self, timestamp: str) -> str:
        """Helper method to format timestamp as 'X time ago' string"""
        if not timestamp:
            return "Never"
            
        try:
            from datetime import datetime
            if isinstance(timestamp, str):
                timestamp_dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
            else:
                timestamp_dt = datetime.fromtimestamp(timestamp)
            
            # Calculate time difference
            now = datetime.now(timestamp_dt.tzinfo) if timestamp_dt.tzinfo else datetime.now()
            time_diff = now - timestamp_dt
            
            if time_diff.days > 0:
                return f"{time_diff.days} day(s) ago"
            elif time_diff.seconds > 3600:
                hours = time_diff.seconds // 3600
                return f"{hours} hour(s) ago"
            elif time_diff.seconds > 60:
                minutes = time_diff.seconds // 60
                return f"{minutes} minute(s) ago"
            else:
                return "Just now"
                
        except Exception as e:
            self.print_debug(f"Error parsing timestamp '{timestamp}': {e}")
            return "Unknown"

    def combined_node_status_and_info(self) -> None:
        """Display beautiful live container status overview - checks if Edge Node containers are running"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        # Load configuration
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        # Clear screen and show loading
        print("\033[2J\033[H", end="")
        self.print_header("Container Status")
        self.print_colored("🔍 Checking container status (max 30s timeout)...", 'cyan')
        
        # Use the same deployment status workflow - get real-time status
        node_status_data = self._get_real_time_node_status()

        # Update persistent status information for each node
        for node_name, status_data in node_status_data.items():
            self._update_node_status(node_name, status_data['status'])

        # Clear and display beautiful status
        print("\033[2J\033[H", end="")
        self.print_header("Container Status")
        
        # Show deployment status overview (like deployment menu does)
        self._load_active_config()
        deployment_status = self.active_config.get('deployment_status', 'never_deployed')
        last_deployed_date = self.active_config.get('last_deployed_date')
        last_deployed_network = self.active_config.get('last_deployed_network')
        last_deployment_type = self.active_config.get('last_deployment_type')
        last_deleted_date = self.active_config.get('last_deleted_date')
        
        # Overall deployment status
        if deployment_status == 'deployed' and last_deployed_date:
            try:
                deployed_dt = datetime.fromisoformat(last_deployed_date.replace('Z', '+00:00'))
                deployed_str = deployed_dt.strftime('%Y-%m-%d %H:%M')
                deployment_text = f"🚀 Last deployed: {deployed_str}"
                if last_deployed_network:
                    deployment_text += f" ({last_deployed_network})"
                self.print_colored(f"Deployment: {deployment_text}", 'green')
            except:
                self.print_colored("Deployment: ✓ Deployed", 'green')
        elif deployment_status == 'deleted' and last_deleted_date:
            try:
                deleted_dt = datetime.fromisoformat(last_deleted_date.replace('Z', '+00:00'))
                deleted_str = deleted_dt.strftime('%Y-%m-%d %H:%M')
                deployment_text = f"🗑️ Last deleted: {deleted_str}"
                self.print_colored(f"Deployment: {deployment_text}", 'red')
            except:
                self.print_colored("Deployment: ✓ Deleted", 'red')
        else:
            self.print_colored("Deployment: ✗ Never deployed", 'yellow')
        print()
        
        # Network info
        env_color = 'green' if env else 'red'
        env_text = env if env else 'Not Set'
        self.print_colored(f"🌐 {env_text} │ 🐳 {len(hosts)} containers", env_color, bold=True)
        print()

        # Count status
        running = sum(1 for data in node_status_data.values() if data['status'] == 'running')
        stopped = sum(1 for data in node_status_data.values() if data['status'] == 'stopped')
        unreachable = sum(1 for data in node_status_data.values() if data['status'] == 'unreachable')
        not_deployed = sum(1 for data in node_status_data.values() if data['status'] == 'not_deployed')
        unknown = sum(1 for data in node_status_data.values() if data['status'] == 'unknown')

        # Status overview
        self.print_colored("┌─ Container Status " + "─" * 48 + "┐", 'cyan')
        status_line = "│"
        if running > 0:
            status_line += f" 🟢 {running} Running"
        if stopped > 0:
            status_line += f" 🔴 {stopped} Stopped"
        if not_deployed > 0:
            status_line += f" 📦 {not_deployed} Not Deployed"
        if unreachable > 0:
            status_line += f" 🔌 {unreachable} Unreachable"
        if unknown > 0:
            status_line += f" ❓ {unknown} Unknown"
        
        # Pad the status line
        padding = 68 - len(status_line)
        status_line += " " * padding + "│"
        
        if running > 0:
            self.print_colored(status_line, 'green')
        elif stopped > 0:
            self.print_colored(status_line, 'red')
        else:
            self.print_colored(status_line, 'yellow')
        
        self.print_colored("└" + "─" * 68 + "┘", 'cyan')
        print()

        # Container list
        self.print_colored("┌─ Containers " + "─" * 55 + "┐", 'cyan')
        
        for i, (name, config) in enumerate(hosts.items()):
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            status_data = node_status_data[name]
            status = status_data['status']
            
            # Status display
            if status == 'running':
                status_icon = "🟢"
                status_text = "RUNNING"
                status_color = 'green'
            elif status == 'stopped':
                status_icon = "🔴"
                status_text = "STOPPED"
                status_color = 'red'
            elif status == 'not_deployed':
                status_icon = "📦"
                status_text = "NOT DEPLOYED"
                status_color = 'yellow'
            elif status == 'unreachable':
                status_icon = "🔌"
                status_text = "UNREACHABLE"
                status_color = 'red'
            else:
                status_icon = "❓"
                status_text = "UNKNOWN"
                status_color = 'yellow'
            
            # Node name line
            self.print_colored("│", 'cyan', end='')
            self.print_colored(f" {status_icon} {name}", status_color, bold=True, end='')
            self.print_colored(f" [{status_text}]", status_color, end='')
            
            # Calculate padding for node name line
            line_text = f" {status_icon} {name} [{status_text}]"
            padding = 67 - len(line_text)
            self.print_colored(" " * padding + "│", 'cyan')
            
            # Connection info
            self.print_colored("│", 'cyan', end='')
            self.print_colored(f"   📡 {user}@{ip}", 'white', end='')
            
            display_text = f"   📡 {user}@{ip}"
            padding = 67 - len(display_text)
            self.print_colored(" " * padding + "│", 'cyan')
            
            # Add separator between nodes (except last)
            if i < len(hosts) - 1:
                self.print_colored("├" + "─" * 68 + "┤", 'cyan')
        
        self.print_colored("└" + "─" * 68 + "┘", 'cyan')
        print()

        # Quick actions
        self.print_colored("💡 Quick Actions: 3 Start │ 4 Stop │ 5 Restart", 'white')

        input("\nPress Enter to continue...")

    def _manage_service(self, playbook_name: str, title: str, action_text: str) -> None:
        """Common method to manage Edge Nodes"""
        if not self.check_hosts_config():
            self.print_colored("No nodes configured! Please configure nodes first.", 'red')
            input("Press Enter to continue...")
            return

        self.print_header(title)

        # Load configuration to show service management details
        self.load_configuration()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        # Show service management details
        self.print_colored(f"🔧 Service Management Details:", 'cyan', bold=True)
        self.print_colored(f"   • Action: {action_text}", 'white')
        self.print_colored(f"   • Network: {env if env else 'Not set'}", 'green' if env else 'red')
        self.print_colored(f"   • Available Nodes: {len(hosts)}", 'white')

        self.print_colored(f"\n🖥️  Available Machines:", 'cyan', bold=True)
        for name, config in hosts.items():
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            status_info = self._get_node_status_info(name)
            status = status_info['status']
            emoji, color, description = self._get_status_display_info(status)
            self.print_colored(f"   • {name}: {user}@{ip} ", 'white', end='')
            self.print_colored(f"[{emoji} {description}]", color, end='')

        # Service-specific descriptions
        if "start" in playbook_name:
            self.print_colored(f"\n📋 This will:", 'yellow', bold=True)
            self.print_colored("   • Start the Edge Node systemd service on selected nodes", 'yellow')
            self.print_colored("   • Enable the service to start automatically on boot", 'yellow')
            self.print_colored("   • Verify service startup status", 'yellow')
        elif "stop" in playbook_name:
            self.print_colored(f"\n📋 This will:", 'yellow', bold=True)
            self.print_colored("   • Stop the Edge Node systemd service on selected nodes", 'yellow')
            self.print_colored("   • Keep the service enabled for future startups", 'yellow')
            self.print_colored("   • Verify service shutdown status", 'yellow')
        elif "restart" in playbook_name:
            self.print_colored(f"\n📋 This will:", 'yellow', bold=True)
            self.print_colored("   • Stop the current Edge Node", 'yellow')
            self.print_colored("   • Start the Edge Node with fresh state", 'yellow')
            self.print_colored("   • Verify service restart status", 'yellow')
        elif "status" in playbook_name:
            self.print_colored(f"\n📋 This will:", 'yellow', bold=True)
            self.print_colored("   • Check the current status of Edge Node", 'yellow')
            self.print_colored("   • Show service logs and runtime information", 'yellow')
            self.print_colored("   • Display resource usage if available", 'yellow')

        if not env:
            self.print_colored("\n⚠️  WARNING: Network environment is not set!", 'red', bold=True)
            self.print_colored("   Service operations will proceed, but network environment should be configured.", 'red')

        # Interactive host selection
        operation_name = action_text.lower()
        selected_hosts = self._interactive_host_selection(hosts, operation_name)
        
        if not selected_hosts:
            self.print_colored("No hosts selected. Operation cancelled.", 'yellow')
            return

        # Show final confirmation with selected hosts
        self.print_colored(f"\n📋 Selected hosts for {operation_name}:", 'cyan', bold=True)
        for host_name in selected_hosts:
            host_config = hosts[host_name]
            ip = host_config.get('ansible_host', 'Unknown')
            user = host_config.get('ansible_user', 'Unknown')
            self.print_colored(f"   ✓ {host_name}: {user}@{ip}", 'green')

        # Confirmation for potentially disruptive operations
        if "stop" in playbook_name or "restart" in playbook_name:
            if self.get_input(f"\n⚠️  Continue with {operation_name} on {len(selected_hosts)} selected node(s)? (y/n)", "y").lower() != 'y':
                self.print_colored("Service operation cancelled.", 'yellow')
                return
        else:
            if self.get_input(f"\n🔧 Continue with {operation_name} on {len(selected_hosts)} selected node(s)? (y/n)", "y").lower() != 'y':
                self.print_colored("Service operation cancelled.", 'yellow')
                return

        playbook_path = self.config_dir / f'playbooks/{playbook_name}'
        if not playbook_path.exists():
            self.print_colored(f"Service management playbook not found: {playbook_path}", 'red')
            input("Press Enter to continue...")
            return

        # Build Ansible command with host limitation
        cmd = (f"ANSIBLE_CONFIG={os.environ['ANSIBLE_CONFIG']} "
               f"ANSIBLE_COLLECTIONS_PATH={os.environ['ANSIBLE_COLLECTIONS_PATH']} "
               f"ANSIBLE_HOME={os.environ['ANSIBLE_HOME']} "
               f"ansible-playbook -i {self.config_file} {playbook_path}")

        # Add --limit parameter to target only selected hosts
        if len(selected_hosts) < len(hosts):
            limit_hosts = ','.join(selected_hosts)
            cmd += f" --limit '{limit_hosts}'"

        # Update node statuses to reflect the operation being performed
        if "start" in playbook_name or "restart" in playbook_name:
            for host_name in selected_hosts:
                self._update_node_status(host_name, 'deploying')
        elif "stop" in playbook_name:
            for host_name in selected_hosts:
                self._update_node_status(host_name, 'deploying')

        self.print_colored(f"\n{action_text} on {len(selected_hosts)} node(s)...", 'cyan')
        success, _ = self.run_command(cmd, show_output=True)

        if success:
            self.print_colored(f"\n✅ {title} completed successfully!", 'green')
            
            # Update node statuses based on successful operation
            if "start" in playbook_name:
                for host_name in selected_hosts:
                    self._update_node_status(host_name, 'running')
                self.print_colored(f"Edge Nodes have been started on {len(selected_hosts)} node(s).", 'green')
            elif "stop" in playbook_name:
                for host_name in selected_hosts:
                    self._update_node_status(host_name, 'stopped')
                self.print_colored(f"Edge Nodes have been stopped on {len(selected_hosts)} node(s).", 'green')
            elif "restart" in playbook_name:
                for host_name in selected_hosts:
                    self._update_node_status(host_name, 'running')
                self.print_colored(f"Edge Nodes have been restarted on {len(selected_hosts)} node(s).", 'green')
            elif "status" in playbook_name:
                # Status check doesn't change the actual status, just reports it
                self.print_colored(f"Service status information retrieved for {len(selected_hosts)} node(s).", 'green')
                
            # Show updated statuses
            if "start" in playbook_name or "stop" in playbook_name or "restart" in playbook_name:
                self.print_colored(f"\n📊 Updated Node Statuses:", 'cyan', bold=True)
                for host_name in selected_hosts:
                    self.print_colored(f"   • {host_name}: ", 'white', end='')
                    self._display_node_status(host_name, compact=True)
                    print()  # New line after each status
        else:
            self.print_colored(f"\n❌ {title} encountered issues. Please check the output above.", 'red')
            
            # Update node statuses to reflect potential error state
            if "start" in playbook_name or "restart" in playbook_name or "stop" in playbook_name:
                for host_name in selected_hosts:
                    self._update_node_status(host_name, 'error')
                self.print_colored(f"\n📊 Node statuses updated to Error due to operation failure.", 'yellow')
            
            # Additional error guidance based on operation
            if "start" in playbook_name:
                self.print_colored("Common issues: Service not deployed, network connectivity, or configuration errors.", 'yellow')
            elif "stop" in playbook_name or "restart" in playbook_name:
                self.print_colored("This might be normal if the service was not running on some nodes.", 'yellow')

        input("Press Enter to continue...")


if __name__ == "__main__":
    r1setup = R1Setup()
    r1setup.run()
