#!/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

# 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': {}
                    }
                }
            }
        }

        # 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.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:
                    self.active_config.update(json.load(f))
            except Exception as e:
                self.print_colored(f"Warning: Could not load active config: {e}", 'yellow')

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

    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) -> 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)

        # Save metadata
        metadata = {
            'environment': environment,
            'nodes_count': nodes_count,
            'created_at': datetime.now().isoformat(),
            'config_name': config_name,
            'description': f"Configuration with {nodes_count} node(s) for {environment} network",
            'last_deployed_date': None,
            'last_deployed_network': None,
            'deployment_status': 'never_deployed',
            'last_deleted_date': None,
            'last_deployment_type': None
        }

        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
        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) -> tuple:
        """Run a shell command and return success status and output"""
        try:
            if show_output:
                self.print_colored(f"Running: {cmd}", 'cyan')

            result = subprocess.run(
                cmd,
                shell=shell,
                capture_output=not show_output,
                text=True,
                check=False
            )

            if show_output:
                return result.returncode == 0, ""
            else:
                return result.returncode == 0, result.stdout
        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
            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"""
        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"CLI Version: {CLI_VERSION}", 'cyan')
        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 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("Available Options")
        print()
        self.print_colored("Configuration Management:")
        self.print_colored("  1) Configure nodes          - Set up GPU nodes for deployment")
        self.print_colored("  2) Manage configurations    - List, switch, or delete saved configurations")
        self.print_colored("  3) View configuration       - Display current node configuration")
        self.print_colored("  4) Test node connectivity   - Verify connection to configured nodes")
        print()
        self.print_colored("Deployment:")
        self.print_colored("  5) Deploy full setup        - Deploy Docker + NVIDIA drivers + GPU setup")
        self.print_colored("  6) Deploy Docker only       - Deploy only Docker without GPU setup")
        self.print_colored("  7) Delete edge node         - Completely remove deployed edge node")
        self.print_colored("  8) Apply new nodes names    - Update node aliases")
        print()
        self.print_colored("Information:")
        self.print_colored("  9) Get node information     - Retrieve detailed node information")
        self.print_colored(" 10) Get node addresses       - Display nodes addresses")
        self.print_colored(" 11) Export addresses to CSV  - Save addresses to CSV file")
        print()
        self.print_colored("Settings:")
        self.print_colored(" 12) Change network environment - Switch between mainnet/testnet/devnet")
        self.print_colored(" 13) Update CLI & Collection    - Check for and install CLI & Ansible collection updates")
        print()
        self.print_colored("  0) Exit")
        print()

    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("Configuration Options:")
            self.print_colored("  1) Create new configuration  - Set up a new node configuration")
            if configs:
                self.print_colored("  2) Switch configuration      - Activate a different configuration")
                self.print_colored("  3) Delete configuration      - Remove a saved configuration")
                self.print_colored("  4) Rename configuration      - Change configuration name")
            self.print_colored("  0) Back to main menu")
            print()

            choice = self.get_input("Select option")

            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)
            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
                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
                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
                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 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')
                    self.print_colored(f"  {i}. {name} ({user}@{ip})")
                print()

            self.print_colored("Configuration Options:")
            if not hosts:
                self.print_colored("  1) Create initial configuration - Set up your first nodes")
            else:
                self.print_colored("  1) Add new node                 - Add another node to the configuration")
                self.print_colored("  2) Update existing node         - Modify an existing node's settings")
                self.print_colored("  3) Delete node                  - Remove a node from configuration")
                self.print_colored("  4) Create new configuration     - Start over with fresh configuration")
            self.print_colored("  0) Back to main menu")
            print()

            choice = self.get_input("Select option")

            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 _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
                            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']
                            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'

            # 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 _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
                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
            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("\n💡 Recommendation:", 'cyan', bold=True)
            self.print_colored("Since you changed the node name, consider running option 8 (Apply new node names)", 'yellow')
            self.print_colored("from the main menu to update the node name on the network.", 'yellow')
            self.print_colored("This will ensure your node is visible with the new name on the blockchain.", '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
                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)
        else:
            # Update existing config
            config_name = self.active_config['config_name']
            self._save_config_with_metadata(config_name, env, nodes_count)

    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():
                self.print_colored(f"\nNode: {name}", 'yellow')
                for key, value in config.items():
                    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")

        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}")

        success, _ = self.run_command(cmd, show_output=True)

        if success:
            self.print_colored("\nConnectivity test completed successfully!", 'green')
        else:
            self.print_colored("\nConnectivity test failed. Please check your configuration.", 'red')

        input("Press 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"""
        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()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        # 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"   • Target Nodes: {len(hosts)}", 'white')

        self.print_colored(f"\n🖥️  Edge nodes will be deleted from these machines:", 'cyan', bold=True)
        for name, config in hosts.items():
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            self.print_colored(f"   • {name}: {user}@{ip}", '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(hosts)} 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(hosts)} 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

        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("\nStarting edge node deletion...", '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()
        else:
            self.print_colored("\nEdge node deletion encountered issues. Please check the output above.", 'red')

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

    def apply_node_names(self) -> None:
        """Apply new node names (Update node alias configuration)"""
        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("Apply New Node Names")

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

        # Show rename details
        self.print_colored(f"🏷️  Node Renaming Details:", 'cyan', bold=True)
        self.print_colored(f"   • Action: Update node alias configuration for each node", 'white')
        self.print_colored(f"   • Network: {env if env else 'Not set'}", 'green' if env else 'red')
        self.print_colored(f"   • Target Nodes: {len(hosts)}", 'white')

        self.print_colored(f"\n🖥️  Target Machines and Names:", 'cyan', bold=True)
        for name, config in hosts.items():
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            self.print_colored(f"   • {name}: {user}@{ip} → node alias will be set to '{name}'", 'white')

        self.print_colored(f"\n📋 This will:", 'yellow', bold=True)
        self.print_colored("   • Update the startup configuration file with new node alias", 'yellow')
        self.print_colored("   • Restart the edge node service", 'yellow')
        self.print_colored("   • Wait for the service to start", 'yellow')
        self.print_colored("   • Retrieve and display updated node information", 'yellow')

        if not env:
            self.print_colored("\n⚠️  WARNING: Network environment is not set!", 'red', bold=True)
            self.print_colored("   The renaming will proceed, but please ensure network is configured properly.", 'red')

        if self.get_input(f"\n🏷️  Continue with applying new node names to {len(hosts)} node(s)? (y/n)", "y").lower() != 'y':
            self.print_colored("Node renaming cancelled.", 'yellow')
            return

        playbook_path = self.config_dir / 'playbooks/rename_nodes.yml'
        if not playbook_path.exists():
            self.print_colored(f"Rename 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("\nStarting node renaming process...", 'cyan')
        success, _ = self.run_command(cmd, show_output=True)

        if success:
            self.print_colored("\nNode renaming completed successfully!", 'green')
            self.print_colored("All nodes have been updated with their new node alias configuration.", 'green')
        else:
            self.print_colored("\nNode renaming encountered issues. Please check the output above.", 'red')

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

    def _deploy_setup(self, playbook: str, title: str, description: str, extra_vars: str = "") -> None:
        """Common deployment logic"""
        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()
        hosts = self.inventory.get('all', {}).get('children', {}).get('gpu_nodes', {}).get('hosts', {})
        env = self.get_mnl_app_env()

        # 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 if env else 'Not set'}", 'green' if env else 'red')
        self.print_colored(f"   • Target Nodes: {len(hosts)}", 'white')

        self.print_colored(f"\n🖥️  Target Machines:", 'cyan', bold=True)
        for name, config in hosts.items():
            ip = config.get('ansible_host', 'Unknown')
            user = config.get('ansible_user', 'Unknown')
            self.print_colored(f"   • {name}: {user}@{ip}", '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 service", 'yellow')
        elif "Docker only" in description:
            self.print_colored("   • Install Docker and Docker Compose", 'yellow')
            self.print_colored("   • Deploy and start the edge node service (CPU mode)", 'yellow')
        else:
            self.print_colored(f"   • {description}", 'yellow')

        # Check for network change warning
        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"   • The same node addresses will be used:", 'white')
            for name, config in hosts.items():
                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 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

        if self.get_input(f"\n🚀 Continue with deployment to {len(hosts)} 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

        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}")

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

        self.print_colored(f"\nStarting {title.lower()}...", '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)
        else:
            self.print_colored(f"\n{title} encountered issues. Please check the output above.", 'red')

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

    def get_node_info(self) -> None:
        """Get detailed 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 Information")

        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}")

        success, _ = self.run_command(cmd, show_output=True)

        if success:
            self.print_colored("\nNode information retrieved successfully!", 'green')
        else:
            self.print_colored("\nFailed to retrieve node information.", 'red')

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

    def _get_node_info_data(self) -> Dict[str, Dict[str, Any]]:
        """Get node information data by running the get_node_info playbook and parsing output"""
        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}")

        self.print_colored("Retrieving node information...", 'yellow')
        success, output = self.run_command(cmd, show_output=False)

        if not success:
            self.print_colored("Failed to run node info playbook", 'red')
            return {}

        # Debug: show first few lines of output
        output_lines = output.split('\n')
        self.print_debug(f"Parsing output with {len(output_lines)} lines...")

        # Parse the output to extract JSON data using a more robust approach
        node_info = {}
        import re
        
        # Find all instances of node result blocks
        # Look for pattern: ok: [node-name] => { ... "node_info.stdout_lines": [ ... ] }
        
        # Use a different approach: find each node result block
        result_blocks = re.split(r'(?=ok: \[[^\]]+\] => \{)', output)
        
        self.print_debug(f"Split output into {len(result_blocks)} result blocks")
        
        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)
            
            # Check if this block contains node_info.stdout_lines
            if '"node_info.stdout_lines":' not in block:
                continue
            
            try:
                # Extract the JSON lines from stdout_lines array
                stdout_match = re.search(r'"node_info\.stdout_lines":\s*\[(.*?)\]', block, re.DOTALL)
                if not stdout_match:
                    self.print_debug(f"Could not find stdout_lines for {node_name}")
                    continue
                
                stdout_content = stdout_match.group(1)
                
                # Extract all quoted strings from the stdout_lines array
                quoted_lines = re.findall(r'"([^"]*)"', stdout_content)
                
                # Reconstruct the JSON string
                json_str = ''
                for line in quoted_lines:
                    # 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_info[node_name] = 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}")
                # Continue with other nodes

        # If no nodes found, try the line-by-line approach as fallback
        if not node_info:
            self.print_debug("Block parsing failed, trying line-by-line approach...")
            node_info = self._parse_node_info_line_by_line(output)

        return node_info

    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]
                    
                    # 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):
                        i += 1  # Skip the stdout_lines line
                        
                        # Collect JSON lines
                        while i < len(lines):
                            current_line = lines[i].strip()
                            
                            if current_line == ']' or current_line == '}' or current_line.startswith('ok: ['):
                                break
                            
                            # Clean up the JSON line
                            if current_line.startswith('"') and current_line.endswith('",'):
                                clean_line = current_line[1:-2].replace('\\"', '"')
                                json_lines.append(clean_line)
                            elif current_line.startswith('"') and current_line.endswith('"'):
                                clean_line = current_line[1:-1].replace('\\"', '"')
                                json_lines.append(clean_line)
                            
                            i += 1
                        
                        # Try to parse the collected JSON
                        if json_lines:
                            try:
                                json_str = '\n'.join(json_lines)
                                node_data = json.loads(json_str)
                                node_info[node_name] = node_data
                            except json.JSONDecodeError as e:
                                self.print_debug(f"Failed to parse JSON for {node_name}: {e}")
            else:
                i += 1
        
        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")

        # Get node information using the same playbook as option 9
        node_info = self._get_node_info_data()
        
        if not node_info:
            self.print_colored("Failed to retrieve node information!", 'red')
            input("Press Enter to continue...")
            return

        # Display the addresses in a formatted table
        self.print_colored(f"\nFound information for {len(node_info)} node(s):", 'green')
        self.print_colored(f"{'Node Alias':<20} {'Address':<48} {'ETH Address':<42}", 'cyan')
        self.print_colored("-" * 110, 'cyan')
        
        for node_name, info in node_info.items():
            alias = info.get('alias', node_name)
            address = info.get('address', 'N/A')
            eth_address = info.get('eth_address', 'N/A')
            
            self.print_colored(f"{alias:<20} {address:<48} {eth_address:<42}")
        
        if len(node_info) == 0:
            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("\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")

        # Get node information using the same playbook as option 9
        node_info = self._get_node_info_data()
        
        if not node_info:
            self.print_colored("Failed to retrieve node information!", 'red')
            input("Press Enter to continue...")
            return

        self.print_colored(f"Found information for {len(node_info)} node(s)", '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_Alias,Address,ETH_Address,SSH_Host,SSH_User\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, info in node_info.items():
                    alias = info.get('alias', node_name)
                    address = info.get('address', 'N/A')
                    eth_address = info.get('eth_address', 'N/A')
                    
                    # 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')
                    
                    f.write(f'"{alias}","{address}","{eth_address}","{ssh_host}","{ssh_user}"\n')

            self.print_colored(f"Addresses exported to: {csv_file}", 'green')
            self.print_colored(f"CSV contains: Node Alias, Blockchain Address, ETH Address, SSH Host, SSH User", 'cyan')
        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 _update_deployment_metadata(self, deployment_type: str) -> None:
        """Update deployment metadata after successful deployment"""
        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 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

            # 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')
        except Exception as e:
            self.print_colored(f"Warning: Could not update deployment metadata: {e}", 'yellow')

    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'

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

            # Update the collection using ansible-galaxy
            self.print_colored("  Downloading latest collection from Ansible Galaxy...", 'yellow')

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

            # 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)

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

            if result.returncode == 0:
                # Verify the collection was installed/updated
                verify_cmd = [
                    'ansible-galaxy', 'collection', 'list',
                    '--collections-path', str(collections_path)
                ]

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

                if verify_result.returncode == 0 and 'ratio1.multi_node_launcher' in verify_result.stdout:
                    # Extract version information if possible
                    for line in verify_result.stdout.split('\n'):
                        if 'ratio1.multi_node_launcher' in line:
                            parts = line.split()
                            if len(parts) >= 2:
                                version = parts[1]
                                self.print_colored(f"  Updated to collection version: {version}", 'cyan')
                            break
                    return True
                else:
                    self.print_colored("  Warning: Collection update completed but verification failed", 'yellow')
                    return False
            else:
                # Log the error but don't fail the entire update
                error_msg = result.stderr.strip() if result.stderr else "Unknown error"
                self.print_colored(f"  ansible-galaxy command failed: {error_msg}", 'red')

                # 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')

                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')
            return False

    def _check_ansible_collection_version(self) -> Tuple[Optional[str], bool]:
        """Check the current Ansible collection version and if updates are available"""
        try:
            # Check current installed version
            cmd = [
                'ansible-galaxy', 'collection', 'list',
                '--collections-path', str(self.ansible_config_root / 'collections')
            ]

            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)

            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=30,
                env=env
            )

            current_version = None
            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]
                            break

            # Check latest version from Ansible Galaxy
            # For now, we'll just return the current version and assume updates are available
            # In a real implementation, you might query the Ansible Galaxy API
            return current_version, True  # Assume updates might be available

        except Exception as e:
            self.print_colored(f"Error checking Ansible collection version: {e}", 'yellow')
            return None, False

    def update_cli(self) -> None:
        """Check for and install CLI and Ansible collection updates"""
        self.print_header("CLI & Collection Update")

        # Check CLI version
        self.print_colored("🔍 Checking CLI version...", 'yellow')
        self.print_colored(f"Current CLI version: {CLI_VERSION}", 'cyan')
        
        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:
                self.print_colored(f"Latest CLI version: {latest_cli_version}", 'cyan')
                cli_update_available = self._compare_versions(CLI_VERSION, latest_cli_version) < 0
            else:
                self.print_colored("Could not check CLI version", 'red')
        except Exception as e:
            self.print_colored(f"Error checking CLI version: {e}", 'red')

        # Check Ansible collection version
        self.print_colored("\n🔍 Checking Ansible collection version...", 'yellow')
        current_collection_version, collection_update_available = self._check_ansible_collection_version()
        
        if current_collection_version:
            self.print_colored(f"Current collection version: {current_collection_version}", 'cyan')
        else:
            self.print_colored("Collection version unknown or not installed", 'yellow')

        # Show update status
        self.print_colored("\n📋 Update Status:", 'cyan', bold=True)
        
        if cli_update_available:
            self.print_colored(f"   🆕 CLI: Update available ({CLI_VERSION} → {latest_cli_version})", 'green')
        else:
            self.print_colored(f"   ✅ CLI: Up to date ({CLI_VERSION})", 'green')

        if collection_update_available:
            self.print_colored("   🆕 Collection: Update available", 'green')
        else:
            self.print_colored("   ✅ Collection: Up to date", 'green')

        # If no updates available
        if not cli_update_available and not collection_update_available:
            self.print_colored("\n✅ Both CLI and Ansible collection are up to date!", 'green')
            input("Press Enter to continue...")
            return

        # Show update options
        self.print_colored("\n🔧 Update Options:", 'cyan', bold=True)
        update_options = []
        
        if cli_update_available:
            update_options.append("1) Update CLI only")
        if collection_update_available:
            update_options.append("2) Update Ansible collection only")
        if cli_update_available and collection_update_available:
            update_options.append("3) Update both CLI and collection")
        
        update_options.append("0) Cancel")

        for option in update_options:
            self.print_colored(f"   {option}", 'white')

        while True:
            choice = self.get_input("\nSelect update option")
            
            if choice == '0':
                self.print_colored("Update cancelled.", 'yellow')
                return
            elif choice == '1' and cli_update_available:
                self._update_cli_only(latest_cli_version, cli_download_urls, cli_fallback_urls)
                return
            elif choice == '2' and collection_update_available:
                self._update_collection_only()
                return
            elif choice == '3' and cli_update_available and collection_update_available:
                self._update_both(latest_cli_version, cli_download_urls, cli_fallback_urls)
                return
            else:
                self.print_colored("Invalid option. Please try again.", 'red')

    def _update_cli_only(self, latest_version: str, download_urls: Dict[str, str], fallback_urls: Dict[str, str]) -> None:
        """Update only the CLI script"""
        self.print_colored(f"\n🚀 Updating CLI to version {latest_version}...", 'green', bold=True)
        
        if self.get_input(f"Proceed with CLI update? (y/n)", "y").lower() != 'y':
            self.print_colored("CLI update cancelled.", 'yellow')
            return

        success = self._perform_update(latest_version, download_urls, fallback_urls)
        
        if success:
            self.print_colored(f"\n✅ Successfully updated CLI to version {latest_version}!", 'green')
            self.print_colored("The CLI will restart with the new version...", 'cyan')
            input("Press Enter to restart...")
            os.execv(sys.executable, [sys.executable] + sys.argv)
        else:
            self.print_colored("\n❌ CLI update failed. Please try again or update manually.", 'red')
            input("Press Enter to continue...")

    def _update_collection_only(self) -> None:
        """Update only the Ansible collection"""
        self.print_colored(f"\n🚀 Updating Ansible collection...", 'green', bold=True)
        
        if self.get_input(f"Proceed with collection update? (y/n)", "y").lower() != 'y':
            self.print_colored("Collection update cancelled.", 'yellow')
            return

        success = self._update_ansible_collection()
        
        if success:
            self.print_colored("\n✅ Ansible collection updated successfully!", 'green')
        else:
            self.print_colored("\n❌ Ansible collection update failed.", 'red')
        
        input("Press Enter to continue...")

    def _update_both(self, latest_version: str, download_urls: Dict[str, str], fallback_urls: Dict[str, str]) -> None:
        """Update both CLI and Ansible collection"""
        self.print_colored(f"\n🚀 Updating both CLI and Ansible collection...", 'green', bold=True)
        self.print_colored(f"   CLI: {CLI_VERSION} → {latest_version}", 'white')
        self.print_colored(f"   Collection: Will be updated to latest", 'white')
        
        if self.get_input(f"Proceed with both updates? (y/n)", "y").lower() != 'y':
            self.print_colored("Updates cancelled.", 'yellow')
            return

        # Update CLI first
        cli_success = self._perform_update(latest_version, download_urls, fallback_urls)
        
        if cli_success:
            self.print_colored(f"\n✅ Successfully updated both CLI and Ansible collection!", 'green')
            self.print_colored("The CLI will restart with the new version...", 'cyan')
            input("Press Enter to restart...")
            os.execv(sys.executable, [sys.executable] + sys.argv)
        else:
            self.print_colored("\n❌ CLI update failed. Ansible collection may have been updated.", 'red')
            input("Press Enter to continue...")

    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')

            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("  0) Exit")

            while True:
                choice = self.get_input("\nSelect option (0-2)", "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
                            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')
                        continue
                elif choice == '2':
                    # Create new configuration
                    self._create_new_configuration_with_management()
                    return True
                else:
                    self.print_colored("Invalid option. Please enter 0, 1, or 2.", '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')

            self.print_colored("\n🔧 What would you like to do?", 'cyan', bold=True)
            self.print_colored("  1) Create your first configuration")
            self.print_colored("  0) Exit")

            while True:
                choice = self.get_input("\nSelect option (0-1)", "1")

                if choice == '0':
                    self.print_colored("Exiting r1setup.", 'yellow')
                    return False
                elif choice == '1':
                    self._create_new_configuration_with_management()
                    return True
                else:
                    self.print_colored("Invalid option. Please enter 0 or 1.", '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)

        # 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-13)")

                if choice == '0':
                    self.print_colored("Thank you for using Ratio1 Multi-Node Launcher Setup!", 'green')
                    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.test_connectivity()
                elif choice == '5':
                    self.deploy_full()
                elif choice == '6':
                    self.deploy_docker_only()
                elif choice == '7':
                    self.delete_edge_node()
                elif choice == '8':
                    self.apply_node_names()
                elif choice == '9':
                    self.get_node_info()
                elif choice == '10':
                    self.get_node_addresses()
                elif choice == '11':
                    self.export_addresses_csv()
                elif choice == '12':
                    self.change_network_environment()
                elif choice == '13':
                    self.update_cli()
                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...")


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