"""
Core Converter Module

Contains the main YamlForgeConverter class that orchestrates multi-cloud
infrastructure generation using provider-specific implementations.
"""

import os
import re
import yaml
import subprocess
from pathlib import Path

from .credentials import CredentialsManager
from ..utils import find_yamlforge_file
from ..providers.aws import AWSProvider
from ..providers.azure import AzureProvider
from ..providers.gcp import GCPProvider
from ..providers.ibm_classic import IBMClassicProvider  # type: ignore
from ..providers.ibm_vpc import IBMVPCProvider  # type: ignore
from ..providers.oci import OCIProvider
from ..providers.vmware import VMwareProvider
from ..providers.alibaba import AlibabaProvider
from ..providers.openshift import OpenShiftProvider
from ..providers.cnv import CNVProvider


class YamlForgeConverter:
    """Main converter class that orchestrates multi-cloud infrastructure generation."""

    def __init__(self, images_file="mappings/images.yaml", analyze_mode=False, ansible_mode=False):
        """Initialize the converter with mappings and provider modules."""
        self.ansible_mode = ansible_mode
        # Check Terraform version early (skip if in analyze mode)
        if not analyze_mode:
            self.validate_terraform_version()
        
        self.images = self.load_images(images_file)
        self.locations = self.load_locations("mappings/locations.yaml")
        self.flavors = self.load_flavors("mappings/flavors")
        # Load OpenShift-specific flavors from dedicated directory
        openshift_flavors = self.load_flavors("mappings/flavors_openshift")
        self.flavors.update(openshift_flavors)

        self.core_config = self.load_core_config("defaults/core.yaml")

        # Initialize credentials manager
        self.credentials = CredentialsManager()

        # Initialize provider modules (GUID will be set when processing YAML)
        # Note: AWS provider is initialized lazily to avoid credential checks when not needed
        self._aws_provider = None  # Lazy initialization
        self.azure_provider = AzureProvider(self)
        self.gcp_provider = GCPProvider(self)
        self.ibm_classic_provider = IBMClassicProvider(self)
        self.ibm_vpc_provider = IBMVPCProvider(self)
        self.oci_provider = OCIProvider(self)
        self.vmware_provider = VMwareProvider(self)
        self.alibaba_provider = AlibabaProvider(self)
        
        # Initialize OpenShift provider
        self.openshift_provider = OpenShiftProvider(self)
        
        # Initialize CNV provider
        self.cnv_provider = CNVProvider(self)

        # Current YAML data for GUID extraction
        self.current_yaml_data = None
        
        # Cache for resolved regions to prevent multiple validations
        self._region_cache = {}
        
        # No-credentials mode flag (set by main.py)
        self.no_credentials = False
        
        # Track instance costs for total calculation
        self.instance_costs = []
        # Track OpenShift cluster costs for total calculation
        self.openshift_costs = []

    def get_aws_provider(self):
        """Return the AWS provider instance for use by other components."""
        if self._aws_provider is None:
            self._aws_provider = AWSProvider(self)
        return self._aws_provider

    def get_cnv_provider(self):
        """Return the CNV provider instance for use by other components."""
        if not hasattr(self, '_cnv_provider') or self._cnv_provider is None:
            from yamlforge.providers.cnv.base import BaseCNVProvider
            self._cnv_provider = BaseCNVProvider(self)
        return self._cnv_provider

    def _has_rosa_clusters(self, yaml_data):
        """Check if YAML configuration contains any ROSA clusters."""
        if not yaml_data:
            return False
        
        # Check if openshift_clusters is at root level (legacy) or under yamlforge section
        openshift_clusters = yaml_data.get('openshift_clusters', [])
        if not openshift_clusters and 'yamlforge' in yaml_data:
            openshift_clusters = yaml_data['yamlforge'].get('openshift_clusters', [])
        
        for cluster in openshift_clusters:
            cluster_type = cluster.get('type', '').lower()
            if cluster_type in ['rosa-classic', 'rosa-hcp', 'rosa']:
                return True
        return False

    def validate_terraform_version(self):
        """Validate that Terraform is installed and meets minimum version requirements."""
        try:
            # Run terraform version command
            result = subprocess.run(['terraform', '--version'], 
                                  capture_output=True, text=True, timeout=10)
            
            if result.returncode != 0:
                raise ValueError(
                    "Terraform Version Error: Failed to execute 'terraform --version'\n\n"
                    "Please ensure Terraform is installed and available in your PATH:\n\n"
                    "1. Download and install Terraform:\n"
                    "   https://developer.hashicorp.com/terraform/downloads\n\n"
                    "2. Or use package managers:\n"
                    "   # macOS (Homebrew)\n"
                    "   brew install terraform\n\n"
                    "   # Linux (Ubuntu/Debian)\n"
                    "   wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg\n"
                    "   echo \"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/hashicorp.list\n"
                    "   sudo apt update && sudo apt install terraform\n\n"
                    "3️⃣  Verify installation:\n"
                    "   terraform --version"
                )
            
            # Parse version from output (e.g., "Terraform v1.12.2")
            version_output = result.stdout.strip()
            version_match = re.search(r'Terraform v(\d+)\.(\d+)\.(\d+)', version_output)
            
            if not version_match:
                raise ValueError(
                    f"Terraform Version Error: Could not parse version from output:\n{version_output}\n\n"
                    "Please ensure you have a standard Terraform installation."
                )
            
            major, minor, patch = map(int, version_match.groups())
            current_version = (major, minor, patch)
            
            # Minimum required version: 1.12.0 (based on our testing)
            min_version = (1, 12, 0)
            
            if current_version < min_version:
                current_version_str = f"{major}.{minor}.{patch}"
                min_version_str = f"{min_version[0]}.{min_version[1]}.{min_version[2]}"
                
                raise ValueError(
                    f"Terraform Version Error: Version {current_version_str} is too old\n\n"
                    f"Required: Terraform v{min_version_str} or newer\n"
                    f"Current: Terraform v{current_version_str}\n\n"
                    "YamlForge requires a modern Terraform version to resolve provider dependencies correctly.\n\n"
                    "Upgrade Terraform:\n\n"
                    "1. Download latest version:\n"
                    "   https://developer.hashicorp.com/terraform/downloads\n\n"
                    "2. Or use tfswitch for easy version management:\n"
                    "   curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/release/install.sh | bash\n"
                    "   tfswitch\n\n"
                    "3. Update via package managers:\n"
                    "   # macOS (Homebrew)\n"
                    "   brew upgrade terraform\n\n"
                    "   # Linux (Ubuntu/Debian)\n"
                    "   sudo apt update && sudo apt upgrade terraform\n\n"
                    "4. Verify upgrade:\n"
                    "   terraform --version\n\n"
                    "Why this matters: Older Terraform versions have known issues with\n"
                    "   ROSA/OpenShift provider dependency resolution that cause deployment failures."
                )
            
            # Success - version is acceptable
            current_version_str = f"{major}.{minor}.{patch}"
            if not getattr(self, 'ansible_mode', False):
                print(f"Detected Terraform version {current_version_str} (meets minimum requirement)")
            
        except subprocess.TimeoutExpired:
            raise ValueError(
                "Terraform Version Error: Command 'terraform --version' timed out\n\n"
                "This might indicate a problem with your Terraform installation.\n"
                "   Please verify Terraform is properly installed and functional."
            )
        except FileNotFoundError:
            raise ValueError(
                "Terraform Version Error: 'terraform' command not found\n\n"
                "Terraform is required to deploy the generated configurations.\n\n"
                "Install Terraform:\n\n"
                "1. Download from official site:\n"
                "   https://developer.hashicorp.com/terraform/downloads\n\n"
                "2. Or use package managers:\n"
                "   # macOS (Homebrew)\n"
                "   brew install terraform\n\n"
                "   # Linux (Ubuntu/Debian)\n"
                "   wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg\n"
                "   echo \"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/hashicorp.list\n"
                "   sudo apt update && sudo apt install terraform\n\n"
                "   # Windows (Chocolatey)\n"
                "   choco install terraform\n\n"
                "3. Verify installation:\n"
                "   terraform --version\n\n"
                "Note: YamlForge generates Terraform configurations, so Terraform\n"
                "   is essential for deploying your infrastructure."
            )
        except Exception as e:
            raise ValueError(
                f"Terraform Version Error: Unexpected error checking Terraform version:\n{str(e)}\n\n"
                "Please ensure Terraform is properly installed and accessible."
            )

    def get_validated_guid(self, yaml_data=None):
        """Get GUID from environment variable or root-level YAML config with validation."""
        # Return cached GUID if already validated
        if hasattr(self, '_validated_guid') and self._validated_guid:
            return self._validated_guid
        
        # Try environment variable first (highest priority)
        guid = os.environ.get('GUID', '').strip()
        
        # If not in environment, try to get from YAML root level
        if not guid and yaml_data:
            guid = yaml_data.get('guid', '')
        
        # Validate GUID format if provided
        if guid:
            # Normalize to lowercase first
            guid = guid.lower()
            if not self.validate_guid_format(guid):
                raise ValueError(
                    f"Invalid GUID format: '{guid}'. "
                    f"GUID must be exactly 5 characters, alphanumeric only (a-z, 0-9) "
                    f"to comply with DNS RFC and Kubernetes standards. "
                    f"Examples: 'abc12', 'web01', 'k8s99', '12345'"
                )
            # Cache the validated GUID
            self._validated_guid = guid
            return guid
        else:
            # In analyze mode, return a placeholder instead of raising an error
            if hasattr(self, 'analyze_mode') and self.analyze_mode:
                return "analyze"
            else:
                raise ValueError(
                    "GUID is required but not provided.\n\n"
                    "Please choose one of these options:\n\n"
                    "1. Set environment variable (recommended):\n"
                    "   export GUID=web01\n\n"
                    "2. Add to YAML root level:\n"
                    "   guid: \"web01\"\n"
                    "   yamlforge:\n"
                    "     ...\n\n"
                    "GUID Requirements:\n"
                    "   • Exactly 5 characters\n"
                    "   • Lowercase alphanumeric only (a-z, 0-9)\n"
                    "   • Examples: web01, app42, test1, dev99\n\n"
                    "Quick Start:\n"
                    "   export GUID=test1 && ./yamlforge.py your-config.yaml -d output/"
                )

    def validate_guid_format(self, guid):
        """Validate that GUID meets DNS RFC and Kubernetes standards."""
        if not guid:
            return False
        
        # Must be exactly 5 characters
        if len(guid) != 5:
            return False
        
        # Must be lowercase alphanumeric only (a-z, 0-9)
        if not re.match(r'^[a-z0-9]{5}$', guid):
            return False
        
        return True

    def set_yaml_data(self, yaml_data):
        """Set current YAML data for GUID extraction and notify providers."""
        self.current_yaml_data = yaml_data
        
        # Merge YAML configuration overrides with core configuration
        if yaml_data and 'yamlforge' in yaml_data:
            yamlforge_config = yaml_data['yamlforge']
            if 'core' in yamlforge_config:
                # Deep merge the core configuration overrides
                self.merge_core_config_overrides(yamlforge_config['core'])
        
        # Clear GUID cache when new YAML data is set
        if hasattr(self, '_validated_guid'):
            delattr(self, '_validated_guid')
        
        # Update GUID in providers that need it
        if hasattr(self.gcp_provider, 'update_guid'):
            try:
                guid = self.get_validated_guid(yaml_data)
                self.gcp_provider.update_guid(guid)
            except Exception as e:
                # Only raise if we don't already have a valid GUID
                if not hasattr(self, '_validated_guid') or not self._validated_guid:
                    raise

    def merge_core_config_overrides(self, yaml_core_config):
        """Merge YAML core configuration overrides with the loaded core configuration."""
        if not hasattr(self, 'core_config'):
            return
        
        # Deep merge the configurations
        self.core_config = self.deep_merge(self.core_config, yaml_core_config)

    def deep_merge(self, base_dict, override_dict):
        """Deep merge two dictionaries, with override_dict taking precedence."""
        result = base_dict.copy()
        
        for key, value in override_dict.items():
            if key in result and isinstance(result[key], dict) and isinstance(value, dict):
                result[key] = self.deep_merge(result[key], value)
            else:
                result[key] = value
        
        return result

    def load_images(self, file_path):
        """Load image mappings from YAML file."""
        try:
            actual_path = find_yamlforge_file(file_path)
            with open(actual_path, 'r') as f:
                data = yaml.safe_load(f)
                return data.get('images', {})
        except FileNotFoundError:
            print(f"Warning: {file_path} not found. Using empty image mappings.")
            return {}



    def load_locations(self, file_path):
        """Load location mappings from YAML file."""
        try:
            actual_path = find_yamlforge_file(file_path)
            with open(actual_path, 'r') as f:
                data = yaml.safe_load(f)
                return data or {}
        except FileNotFoundError:
            print(f"Warning: {file_path} not found. Using empty location mappings.")
            return {}

    def load_flavors(self, directory_path):
        """Load flavor mappings from directory."""
        flavors = {}
        
        # Try to find the directory using our path resolution
        try:
            # First try if it's an existing directory (development mode)
            flavor_dir = Path(directory_path)
            if not flavor_dir.exists():
                # Try repository mode (relative to this module)
                module_dir = Path(__file__).parent.parent
                flavor_dir = module_dir / directory_path
                
            if flavor_dir.exists():
                for file_path in flavor_dir.glob("*.yaml"):
                    try:
                        with open(file_path, 'r') as f:
                            data = yaml.safe_load(f)
                            if data:
                                cloud_name = file_path.stem
                                if cloud_name == 'generic':
                                    flavors.update(data)
                                else:
                                    flavors[cloud_name] = data
                    except Exception as e:
                        print(f"Warning: Could not load {file_path}: {e}")
            else:
                # For pip installs, try to load individual files from the package
                try:
                    from importlib.resources import files
                    package_files = files('yamlforge')
                    
                    # Common flavor files to try loading
                    flavor_files = [
                        'alibaba.yaml', 'aws.yaml', 'azure.yaml', 'cheapest.yaml',
                        'cnv.yaml', 'gcp.yaml', 'generic.yaml', 'ibm_classic.yaml',
                        'ibm_vpc.yaml', 'oci.yaml', 'vmware.yaml'
                    ]
                    
                    for filename in flavor_files:
                        try:
                            file_path = f"{directory_path}/{filename}"
                            data_content = package_files.joinpath(file_path).read_text()
                            data = yaml.safe_load(data_content)
                            if data:
                                cloud_name = filename.replace('.yaml', '')
                                if cloud_name == 'generic':
                                    flavors.update(data)
                                else:
                                    flavors[cloud_name] = data
                        except (FileNotFoundError, Exception):
                            # Ignore missing flavor files
                            pass
                except (ImportError, Exception):
                    pass
                    
        except Exception as e:
            print(f"Warning: Could not load flavors from {directory_path}: {e}")
            
        return flavors



    def load_core_config(self, file_path):
        """Load core yamlforge configuration from YAML file."""
        try:
            actual_path = find_yamlforge_file(file_path)
            with open(actual_path, 'r') as f:
                data = yaml.safe_load(f)
                config = data or {}
        except FileNotFoundError:
            print(f"Warning: {file_path} not found. Using default core configuration.")
            config = self.get_default_core_config()
        
        # Override with environment variables if present
        self._apply_env_overrides(config)
        return config
    
    def _apply_env_overrides(self, config):
        """Apply environment variable overrides to core configuration."""
        import os
        
        # Check for provider exclusions via environment variable
        # Format: YAMLFORGE_EXCLUDE_PROVIDERS="aws,azure,gcp"
        excluded_providers_env = os.getenv('YAMLFORGE_EXCLUDE_PROVIDERS')
        if excluded_providers_env:
            excluded_providers = [p.strip() for p in excluded_providers_env.split(',') if p.strip()]
            if excluded_providers:
                # Ensure provider_selection exists
                if 'provider_selection' not in config:
                    config['provider_selection'] = {}
                
                # Merge with existing exclusions
                existing_excluded = config['provider_selection'].get('exclude_from_cheapest', [])
                all_excluded = list(set(existing_excluded + excluded_providers))
                config['provider_selection']['exclude_from_cheapest'] = all_excluded
                
                print(f"Environment override: Excluding providers from cheapest analysis: {excluded_providers}")

    def get_default_core_config(self):
        """Get default core configuration when core.yaml is not found."""
        return {
            'provider_selection': {
                'exclude_from_cheapest': [],
                'priority_order': [
                    'oci', 'alibaba', 'gcp', 'aws', 'azure', 
                    'ibm_vpc', 'vmware', 'ibm_classic'
                ]
            },
            'cost_analysis': {
                'default_currency': 'USD',
                'regional_cost_factors': {},
                'provider_cost_factors': {},
                'provider_discounts': {}
            },
            'security': {
                'default_username': 'cloud-user',
                'default_ssh_public_key': '',
                'auto_detect_ssh_keys': False
            }
        }
    
    def apply_discount(self, cost, provider):
        """Apply provider-specific discount to cost if configured."""
        if cost is None:
            return None
        
        # Check for environment variable first (highest priority)
        env_var_name = f"YAMLFORGE_DISCOUNT_{provider.upper()}"
        env_discount = os.environ.get(env_var_name)
        
        discount_percentage = 0
        if env_discount:
            try:
                discount_percentage = float(env_discount)
                # Validate range
                if discount_percentage < 0 or discount_percentage > 100:
                    print(f"Warning: Invalid discount percentage in {env_var_name}={env_discount}. Must be 0-100. Using 0.")
                    discount_percentage = 0
            except ValueError:
                print(f"Warning: Invalid discount percentage in {env_var_name}={env_discount}. Must be a number. Using 0.")
                discount_percentage = 0
        else:
            # Fall back to core config
            provider_discounts = self.core_config.get('cost_analysis', {}).get('provider_discounts', {})
            discount_percentage = provider_discounts.get(provider, 0)
        
        if discount_percentage > 0:
            # Apply discount as percentage (e.g., 15% discount means multiply by 0.85)
            discount_multiplier = (100 - discount_percentage) / 100
            return cost * discount_multiplier
        
        return cost
    
    def get_discount_info(self, provider):
        """Get discount information for a provider."""
        # Check for environment variable first (highest priority)
        env_var_name = f"YAMLFORGE_DISCOUNT_{provider.upper()}"
        env_discount = os.environ.get(env_var_name)
        
        discount_percentage = 0
        discount_source = "core_config"
        
        if env_discount:
            try:
                discount_percentage = float(env_discount)
                # Validate range
                if discount_percentage < 0 or discount_percentage > 100:
                    discount_percentage = 0
                else:
                    discount_source = "environment"
            except ValueError:
                discount_percentage = 0
        else:
            # Fall back to core config
            provider_discounts = self.core_config.get('cost_analysis', {}).get('provider_discounts', {})
            discount_percentage = provider_discounts.get(provider, 0)
        
        return {
            'has_discount': discount_percentage > 0,
            'discount_percentage': discount_percentage,
            'source': discount_source
        }
    
    def _format_cost_with_discount(self, provider, original_cost, discounted_cost):
        """Format cost display showing original and discounted prices if applicable."""
        if original_cost is None or discounted_cost is None:
            return "Cost N/A"
        
        discount_info = self.get_discount_info(provider)
        
        if discount_info['has_discount'] and original_cost != discounted_cost:
            # Show both original and discounted price
            discount_pct = discount_info['discount_percentage']
            return f"${original_cost:.4f}/hour → ${discounted_cost:.4f}/hour ({discount_pct}% discount)"
        else:
            # No discount, show regular price
            return f"${discounted_cost:.4f}/hour"

    def get_ssh_keys(self, yaml_data):
        """Extract SSH keys from YAML configuration."""
        ssh_keys = yaml_data.get('ssh_keys', {})
        
        # Get the default username from core configuration
        default_username = self.core_config.get('security', {}).get('default_username', 'cloud-user')
        
        # Default SSH key configuration if none provided
        if not ssh_keys:
            # Try to get SSH key from environment variables via credentials system
            from yamlforge.core.credentials import CredentialsManager
            creds_manager = CredentialsManager()
            default_ssh_key = creds_manager.get_default_ssh_key()
            
            if default_ssh_key and default_ssh_key.get('available'):
                return {
                    'default': {
                        'public_key': default_ssh_key['public_key'],
                        'username': default_username  # Use configurable default
                    }
                }
            else:
                return {
                    'default': {
                        'public_key': None,
                        'username': default_username  # Use configurable default
                    }
                }
        
        # Ensure all SSH keys have required fields
        normalized_keys = {}
        for key_name, key_config in ssh_keys.items():
            if isinstance(key_config, str):
                # Simple string format: just the public key
                normalized_keys[key_name] = {
                    'public_key': key_config,
                    'username': default_username  # Use configurable default
                }
            elif isinstance(key_config, dict):
                # Full configuration format
                normalized_keys[key_name] = {
                    'public_key': key_config.get('public_key'),
                    'username': key_config.get('username', default_username),  # Use configurable default as fallback
                    'comment': key_config.get('comment', '')
                }
        
        return normalized_keys

    def get_default_ssh_key(self, yaml_data):
        """Get the default SSH key for instances."""
        ssh_keys = self.get_ssh_keys(yaml_data)
        
        # Return the first available SSH key as default
        if ssh_keys:
            first_key_name = next(iter(ssh_keys))
            return ssh_keys[first_key_name]
        
        return None

    def get_instance_ssh_key(self, instance, yaml_data):
        """Get SSH key configuration for a specific instance."""
        ssh_keys = self.get_ssh_keys(yaml_data)
        
        # Check if instance specifies a particular SSH key
        instance_ssh_key = instance.get('ssh_key')
        if instance_ssh_key and instance_ssh_key in ssh_keys:
            return ssh_keys[instance_ssh_key]
        
        # Fall back to default SSH key
        return self.get_default_ssh_key(yaml_data)

    def get_effective_providers(self, include_excluded=False, instance_exclusions=None):
        """Get list of providers that can be used for cheapest provider selection."""
        all_providers = ['aws', 'azure', 'gcp', 'ibm_vpc', 'ibm_classic', 'oci', 'vmware', 'alibaba']
        
        if include_excluded:
            return all_providers
            
        # Start with global exclusions from core config
        excluded_providers = self.core_config.get('provider_selection', {}).get('exclude_from_cheapest', [])
        
        # Add instance-specific exclusions if provided
        if instance_exclusions:
            excluded_providers = list(set(excluded_providers + instance_exclusions))
        
        available_providers = [p for p in all_providers if p not in excluded_providers]
        
        return available_providers

    def log_provider_exclusions(self, analysis_type, suppress_output=False, instance_exclusions=None, suppress_exclusions=False):
        """Log information about provider exclusions for cheapest provider selection."""
        if suppress_output or suppress_exclusions:
            return
            
        # Get global exclusions
        global_excluded = self.core_config.get('provider_selection', {}).get('exclude_from_cheapest', [])
        
        # Combine with instance-specific exclusions
        all_excluded = list(set(global_excluded + (instance_exclusions or [])))
        
        if all_excluded:
            excluded_list = ', '.join(all_excluded)
            print(f"   Per Instance provider exclusions for {analysis_type}: {excluded_list} (excluded from cost comparison)")
            # Don't show available providers here since they're shown in the main analysis

    def detect_required_providers(self, yaml_data):
        """Detect which cloud providers are actually being used."""
        providers_in_use = set()

        # Check deployments
        deployments = yaml_data.get('deployments', {})
        for deployment_config in deployments.values():
            provider = deployment_config.get('provider')
            if provider and provider != 'cheapest':
                providers_in_use.add(provider)

        # Check instances
        instances = yaml_data.get('instances', [])
        for instance in instances:
            provider = instance.get('provider')
            if provider:
                # Resolve cheapest provider to actual provider
                if provider == 'cheapest':
                    resolved_provider = self.find_cheapest_provider(instance, suppress_output=True)
                    providers_in_use.add(resolved_provider)
                elif provider == 'cheapest-gpu':
                    resolved_provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
                    providers_in_use.add(resolved_provider)
                else:
                    providers_in_use.add(provider)

        # Check OpenShift clusters
        openshift_clusters = yaml_data.get('openshift_clusters', [])
        
        for cluster in openshift_clusters:
            cluster_type = cluster.get('type')
            if cluster_type:
                # Map OpenShift types to underlying cloud providers
                if cluster_type in ['rosa-classic', 'rosa-hcp']:
                    providers_in_use.add('aws')
                elif cluster_type == 'aro':
                    providers_in_use.add('azure')
                elif cluster_type == 'self-managed':
                    # Self-managed uses the specified provider
                    self_managed_provider = cluster.get('provider', 'aws')
                    providers_in_use.add(self_managed_provider)
                elif cluster_type == 'openshift-dedicated':
                    # Dedicated can run on multiple clouds, check cloud_provider
                    cloud_provider = cluster.get('cloud_provider', 'aws')
                    providers_in_use.add(cloud_provider)
                elif cluster_type == 'self-managed':
                    # Self-managed uses existing instances, providers already detected above
                    pass
                elif cluster_type == 'hypershift':
                    # HyperShift worker nodes use the specified provider
                    hypershift_provider = cluster.get('provider', 'aws')
                    providers_in_use.add(hypershift_provider)

        # Check OpenShift clusters and add required providers
        # Note: openshift_clusters was already collected above
        if openshift_clusters:
            # Always need these for OpenShift
            providers_in_use.update(['kubernetes', 'helm'])
            
            # Check ROSA deployment method to determine if RHCS provider is needed
            rosa_deployment = yaml_data.get('rosa_deployment', {})
            deployment_method = rosa_deployment.get('method', 'terraform')  # default to terraform
            
            for cluster in openshift_clusters:
                cluster_type = cluster.get('type', '')
                
                # ROSA clusters with Terraform method need RHCS provider
                if cluster_type in ['rosa-classic', 'rosa-hcp'] and deployment_method == 'terraform':
                    providers_in_use.add('rhcs')
                
                # Add kubectl for self-managed and HyperShift
                if cluster_type in ['self-managed', 'hypershift']:
                    providers_in_use.add('kubectl')
                
                # ARO uses standard Azure provider (azurerm)
                if cluster_type == 'aro':
                    providers_in_use.add('azure')
                
                # Add cloud provider for the cluster
                if cluster_type in ['rosa-classic', 'rosa-hcp']:
                    providers_in_use.add('aws')
                elif cluster_type == 'aro':
                    providers_in_use.add('azure')
                elif cluster_type == 'openshift-dedicated':
                    cloud_provider = cluster.get('cloud_provider')
                    if cloud_provider:
                        providers_in_use.add(cloud_provider)
                elif cluster_type in ['self-managed', 'hypershift']:
                    cloud_provider = cluster.get('provider')
                    if cloud_provider:
                        providers_in_use.add(cloud_provider)

        return sorted(list(providers_in_use))

    def validate_provider_setup(self, yaml_data):
        """Validate cloud provider setup early to catch issues before processing."""
        
        # Validate that cloud_workspace.name is required for all YamlForge configurations
        # Handle both full YAML data and yamlforge section
        if 'yamlforge' in yaml_data:
            # Full YAML data passed
            yamlforge_data = yaml_data.get('yamlforge', {})
        else:
            # Only yamlforge section passed
            yamlforge_data = yaml_data
        
        cloud_workspace = yamlforge_data.get('cloud_workspace', {})
        workspace_name = cloud_workspace.get('name')
        
        if not workspace_name:
            raise ValueError(
                "Configuration Error: 'yamlforge.cloud_workspace.name' is required for all YamlForge configurations.\n\n"
                "Add this to your YAML file:\n"
                "yamlforge:\n"
                "  cloud_workspace:\n"
                "    name: \"your-workspace-name\"\n"
                "    description: \"Optional description\"\n\n"
                "The workspace name is used for:\n"
                "  • Resource naming and organization\n"
                "  • CNV namespace creation\n"
                "  • Cost tracking and management\n"
                "  • Multi-cloud resource grouping"
            )
        
        # Validate that either instances or openshift_clusters are required under yamlforge
        yamlforge_data = yaml_data.get('yamlforge', {})
        instances = yamlforge_data.get('instances', [])
        openshift_clusters = yamlforge_data.get('openshift_clusters', [])
        
        if not instances and not openshift_clusters:
            raise ValueError(
                "Configuration Error: Either 'instances' or 'openshift_clusters' (or both) are required in YamlForge configurations.\n\n"
                "Add at least one of these to your YAML file:\n\n"
                "For cloud instances:\n"
                "yamlforge:\n"
                "  cloud_workspace:\n"
                "    name: \"your-workspace-name\"\n"
                "  instances:\n"
                "    - name: \"my-instance-{guid}\"\n"
                "      provider: \"aws\"\n"
                "      flavor: \"small\"\n"
                "      image: \"RHEL9-latest\"\n"
                "      region: \"us-east-1\"\n"
                "      ssh_key: \"my-key\"\n\n"
                "For OpenShift clusters:\n"
                "yamlforge:\n"
                "  cloud_workspace:\n"
                "    name: \"your-workspace-name\"\n"
                "  openshift_clusters:\n"
                "    - name: \"my-cluster-{guid}\"\n"
                "      type: \"rosa-classic\"\n"
                "      region: \"us-east-1\"\n"
                "      size: \"small\"\n\n"
                "YamlForge is designed to deploy both cloud instances and OpenShift clusters."
            )
        
        required_providers = self.detect_required_providers(yaml_data)
        
        # Validate that all instances have required fields
        instances = yamlforge_data.get('instances', [])
        for instance in instances:
            provider = instance.get('provider')
            if provider and provider != 'cnv':  # CNV doesn't need regions
                # Check if instance has region or location
                has_region = 'region' in instance
                has_location = 'location' in instance
                
                if not has_region and not has_location:
                    instance_name = instance.get('name', 'unnamed')
                    raise ValueError(
                        f"Configuration Error: Instance '{instance_name}' (provider: {provider}) must specify either 'region' or 'location'.\n\n"
                        f"Add one of these to your instance configuration:\n"
                        f"  region: \"us-east-1\"  # Direct region specification\n"
                        f"  location: \"east\"     # Location mapping (see mappings/locations.yaml)\n\n"
                        f"Available locations: {', '.join(sorted(self.locations.keys()))}\n"
                        f"Common regions: us-east-1, us-west-2, eu-west-1, ap-southeast-1"
                    )
                
                if has_region and has_location:
                    instance_name = instance.get('name', 'unnamed')
                    raise ValueError(
                        f"Configuration Error: Instance '{instance_name}' cannot specify both 'region' and 'location'.\n\n"
                        f"Choose one:\n"
                        f"  region: \"us-east-1\"  # Direct region specification\n"
                        f"  location: \"east\"     # Location mapping"
                    )
        
        # Check each required provider
        for provider in required_providers:
            if provider == 'aws':
                # Check if user wants to skip validation
                yamlforge_config = yaml_data.get('yamlforge', {})
                aws_config = yamlforge_config.get('aws', {})
                use_data_sources = aws_config.get('use_data_sources', False)
                
                if not use_data_sources:
                    # Only validate if not using data source fallback
                    try:
                        self.get_aws_provider().validate_aws_setup()
                    except ValueError as e:
                        # Re-raise with context about which instances need AWS
                        aws_instances = []
                        for instance in yaml_data.get('instances', []):
                            instance_provider = instance.get('provider')
                            if instance_provider == 'aws':
                                aws_instances.append(instance.get('name', 'unnamed'))
                            elif instance_provider in ['cheapest', 'cheapest-gpu']:
                                # These might resolve to AWS
                                aws_instances.append(f"{instance.get('name', 'unnamed')} (via {instance_provider})")
                        
                        instance_list = ', '.join(aws_instances) if aws_instances else 'OpenShift clusters'
                        
                        error_msg = str(e).replace("AWS Provider Error:", f"AWS Provider Error (needed for: {instance_list}):")
                        raise ValueError(error_msg) from e
            
            # Add validation for other providers as needed
            # elif provider == 'azure':
            #     self.azure_provider.validate_azure_setup()
            # elif provider == 'gcp':
            #     self.gcp_provider.validate_gcp_setup()
            elif provider == 'cnv':
                # Get CNV configuration
                yamlforge_config = yaml_data.get('yamlforge', {})
                cnv_config = yamlforge_config.get('cnv', {})
                validate_operator = cnv_config.get('validate_operator', True)
                
                # Skip CNV validation for now to avoid hanging
                print("CNV operator validation skipped (temporarily disabled)")
                
                # TODO: Re-enable validation once kubectl timeout issues are resolved
                # if validate_operator:
                #     try:
                #         # Import CNV provider and validate operator
                #         from yamlforge.providers.cnv.base import BaseCNVProvider
                #         cnv_provider = BaseCNVProvider(self)
                #         if not cnv_provider.validate_cnv_operator():
                #             raise ValueError("CNV/KubeVirt operator is not installed or not working")
                #     except ImportError:
                #         print("Warning: CNV provider not available for validation")
                #     except Exception as e:
                #         # Get CNV instances for error context
                #         cnv_instances = []
                #         for instance in yaml_data.get('instances', []):
                #             if instance.get('provider') == 'cnv':
                #                 cnv_instances.append(instance.get('name', 'unnamed'))
                #         
                #         instance_list = ', '.join(cnv_instances) if cnv_instances else 'CNV instances'
                #         error_msg = f"CNV Provider Error (needed for: {instance_list}): {str(e)}"
                #         raise ValueError(error_msg) from e
                # else:
                #     print("CNV operator validation skipped (validate_operator: false)")

    def validate_instance_names(self, instances):
        """Validate that no two instances have the same name within the same cloud provider."""
        # Track instances by provider
        provider_instances = {}
        
        for instance in instances:
            instance_name = instance.get('name')
            if not instance_name:
                continue
                
            provider = instance.get('provider')
            if not provider:
                continue
                
            # Resolve meta providers to actual providers
            if provider == 'cheapest':
                resolved_provider = self.find_cheapest_provider(instance, suppress_output=True)
            elif provider == 'cheapest-gpu':
                resolved_provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
            else:
                resolved_provider = provider
            
            # Initialize provider list if not exists
            if resolved_provider not in provider_instances:
                provider_instances[resolved_provider] = []
            
            # Check for duplicate names in this provider
            for existing_instance_name in provider_instances[resolved_provider]:
                if instance_name == existing_instance_name:
                    raise ValueError(
                        f"Error: Duplicate instance name '{instance_name}' found for provider '{resolved_provider}'. "
                        f"Instance names must be unique within each cloud provider to avoid Terraform resource conflicts."
                    )
            
            # Add this instance name to the provider list
            provider_instances[resolved_provider].append(instance_name)

    def validate_meta_provider_configuration(self, instances):
        """Validate that meta providers are not used with 'region' field."""
        meta_providers = ['cheapest', 'cheapest-gpu']
        
        for instance in instances:
            provider = instance.get('provider')
            instance_name = instance.get('name', 'unnamed')
            
            if provider in meta_providers:
                if 'region' in instance:
                    raise ValueError(
                        f"Configuration Error: Instance '{instance_name}' uses meta provider '{provider}' "
                        f"with 'region' field. Meta providers automatically select the optimal cloud provider "
                        f"and region based on cost analysis.\n\n"
                        f"Fix: Replace 'region' with 'location' for geographic preference:\n"
                        f"   # Wrong:\n"
                        f"   region: \"{instance['region']}\"\n\n"
                        f"   # Correct:\n"
                        f"   location: \"us-east\"  # Geographic preference\n\n"
                        f"Meta provider behavior:\n"
                        f"   • '{provider}' evaluates all cloud providers\n"
                        f"   • Selects cheapest option across AWS, GCP, Azure, etc.\n"
                        f"   • Uses 'location' for geographic guidance only\n"
                        f"   • Chooses optimal region within selected cloud provider"
                    )

    def validate_ibm_cloud_region_consistency(self, instances):
        """Validate that all IBM Cloud instances use the same region due to Terraform provider limitations."""
        ibm_instances = []
        
        # IBM Classic datacenter to region mapping
        ibm_classic_datacenter_regions = {
            'dal10': 'us-south', 'dal12': 'us-south', 'dal13': 'us-south',
            'wdc04': 'us-east', 'wdc06': 'us-east', 'wdc07': 'us-east',
            'sjc03': 'us-west', 'sjc04': 'us-west',
            'lon04': 'eu-gb', 'lon05': 'eu-gb', 'lon06': 'eu-gb',
            'fra02': 'eu-de', 'fra04': 'eu-de', 'fra05': 'eu-de',
            'tok02': 'jp-tok', 'tok04': 'jp-tok', 'tok05': 'jp-tok',
            'syd01': 'au-syd', 'syd04': 'au-syd', 'syd05': 'au-syd',
            'sao01': 'br-sao', 'sao04': 'br-sao', 'sao05': 'br-sao',
            'tor01': 'ca-tor', 'tor04': 'ca-tor', 'tor05': 'ca-tor'
        }
        
        for instance in instances:
            provider = instance.get('provider')
            if provider in ['ibm_vpc', 'ibm_classic']:
                instance_name = instance.get('name', 'unnamed')
                resolved_region = self._resolve_instance_region_silent(instance, provider)
                
                # Normalize IBM Classic datacenter names to region names
                if provider == 'ibm_classic' and resolved_region in ibm_classic_datacenter_regions:
                    normalized_region = ibm_classic_datacenter_regions[resolved_region]
                else:
                    normalized_region = resolved_region
                
                ibm_instances.append({
                    'name': instance_name,
                    'provider': provider,
                    'region': resolved_region,
                    'normalized_region': normalized_region
                })
        
        if len(ibm_instances) <= 1:
            return  # No validation needed for single or no IBM instances
        
        # Check if all instances have the same normalized region
        first_normalized_region = ibm_instances[0]['normalized_region']
        conflicting_instances = []
        
        for instance in ibm_instances[1:]:
            if instance['normalized_region'] != first_normalized_region:
                conflicting_instances.append(instance)
        
        if conflicting_instances:
            # Build detailed error message
            error_msg = (
                f"IBM Cloud Configuration Error: Multiple instances with different regions detected.\n\n"
                f"IBM Cloud Limitation: All IBM Cloud resources in a single Terraform configuration must be in the same region.\n\n"
                f"First instance region: {ibm_instances[0]['region']} ({ibm_instances[0]['name']} - {ibm_instances[0]['provider']})\n"
                f"Normalized region: {first_normalized_region}\n\n"
                f"Conflicting instances:\n"
            )
            
            for instance in conflicting_instances:
                error_msg += f"  • {instance['name']} ({instance['provider']}): {instance['region']} (normalized: {instance['normalized_region']})\n"
            
            error_msg += (
                f"\nSolutions:\n"
                f"1. Use the same region for all IBM Cloud instances (recommended: {first_normalized_region})\n"
                f"2. Split your configuration into separate YAML files with different regions\n"
                f"3. Use different GUIDs for each region-specific deployment\n\n"
                f"Example fix - update all IBM instances to use region '{first_normalized_region}':\n"
                f"  instances:\n"
                f"    - name: \"vm1\"\n"
                f"      provider: \"ibm_vpc\"\n"
                f"      region: \"{first_normalized_region}\"  # Use consistent region\n"
                f"    - name: \"vm2\"\n"
                f"      provider: \"ibm_classic\"\n"
                f"      location: \"wdc07\"  # Maps to {first_normalized_region} region\n"
            )
            
            raise ValueError(error_msg)



    def generate_complete_terraform(self, yaml_data, required_providers, full_yaml_data=None):
        """Generate complete Terraform configuration with regional infrastructure."""
        # Use full_yaml_data if provided, otherwise fall back to yaml_data
        effective_yaml_data = full_yaml_data if full_yaml_data is not None else yaml_data
        
        # Ensure YAML data is set for GUID extraction
        if self.current_yaml_data != effective_yaml_data:
            self.set_yaml_data(effective_yaml_data)
        
        terraform_content = f'''# Generated by YamlForge v2.0 - Regional Multi-Cloud Infrastructure
# Required providers: {', '.join(required_providers)}
# Regional security groups and networking included

terraform {{
  required_version = ">= 1.12.0"
  required_providers {{'''

        # Add provider configurations
        ibm_provider_added = False  # Track if IBM provider has been added
        for provider in required_providers:
            if provider == 'aws':
                terraform_content += '''
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }'''
            elif provider == 'azure':
                terraform_content += '''
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 2.0"
    }'''
            elif provider == 'gcp':
                terraform_content += '''
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0"
    }
    time = {
      source  = "hashicorp/time"
      version = "~> 0.9"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.4"
    }'''
            elif provider in ['ibm_vpc', 'ibm_classic']:
                # Only add IBM provider once, even if both ibm_vpc and ibm_classic are used
                if not ibm_provider_added:
                    terraform_content += '''
    ibm = {
      source  = "IBM-Cloud/ibm"
      version = "~> 1.0"
    }'''
                    ibm_provider_added = True
            elif provider == 'oci':
                terraform_content += '''
    oci = {
      source  = "oracle/oci"
      version = "~> 5.0"
    }'''
            elif provider == 'vmware':
                terraform_content += '''
    vsphere = {
      source  = "hashicorp/vsphere"
      version = "~> 2.4"
    }'''
            elif provider == 'alibaba':
                terraform_content += '''
    alicloud = {
      source  = "aliyun/alicloud"
      version = "~> 1.0"
    }'''
            elif provider == 'rhcs':
                terraform_content += '''
    rhcs = {
      source  = "terraform-redhat/rhcs"
      version = ">= 1.0.1"
    }'''
            elif provider == 'kubernetes':
                terraform_content += '''
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.23"
    }'''
            elif provider == 'helm':
                terraform_content += '''
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.11"
    }'''
            elif provider == 'kubectl':
                terraform_content += '''
    kubectl = {
      source  = "gavinbunney/kubectl"
      version = "~> 1.14"
    }'''
            elif provider == 'cnv':
                terraform_content += '''
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.23"
    }
    kubectl = {
      source  = "gavinbunney/kubectl"
      version = "~> 1.14"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.11"
    }'''

        terraform_content += '''
  }
}

'''

        # Add provider configurations for each required provider
        ibm_provider_config_added = False  # Track if IBM provider config has been added
        for provider in required_providers:
            if provider == 'aws':
                # Generate providers for all AWS regions used
                aws_regions = self.get_all_aws_regions(yaml_data)
                primary_region = aws_regions[0]
                
                terraform_content += f'''# AWS Provider Configuration (Multi-Region Support)
# Primary provider for region: {primary_region}
provider "aws" {{
  region = "{primary_region}"
}}

'''
                
                # Generate aliased providers for additional regions
                for region in aws_regions[1:]:
                    clean_region = region.replace("-", "_").replace(".", "_")
                    terraform_content += f'''# AWS Provider alias for region: {region}
provider "aws" {{
  alias  = "{clean_region}"
  region = "{region}"
}}

'''
                
                terraform_content += '''# AWS Caller Identity for account information
data "aws_caller_identity" "current" {}

'''
            elif provider == 'azure':
                terraform_content += '''# Azure Provider Configuration
provider "azurerm" {
  features {}
  subscription_id = var.azure_subscription_id
  
  # Use environment variables for authentication
  # Set ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID
  # Or use Azure CLI with: az login
}

# Azure Active Directory Provider (required for ARO)
provider "azuread" {
  # Use environment variables for authentication
  # Set ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID
  # Or use Azure CLI with: az login
}

'''
            elif provider == 'gcp':
                # Use existing service account project for provider, then create new project
                terraform_content += '''# GCP Provider Configuration
provider "google" {
  # Uses existing project from service account credentials for provider operations
  # New project will be created using this provider context
  region  = var.gcp_region
}

'''
            elif provider in ['ibm_vpc', 'ibm_classic']:
                # Only add IBM provider configuration once, even if both ibm_vpc and ibm_classic are used
                if not ibm_provider_config_added:
                    terraform_content += '''# IBM Cloud Provider Configuration
provider "ibm" {
  ibmcloud_api_key = var.ibm_api_key
  region           = var.ibm_region
}

'''
                    ibm_provider_config_added = True
            elif provider == 'oci':
                terraform_content += '''# Oracle Cloud Infrastructure Provider Configuration
provider "oci" {
  tenancy_ocid     = var.oci_tenancy_ocid
  user_ocid        = var.oci_user_ocid
  fingerprint      = var.oci_fingerprint
  private_key_path = var.oci_private_key_path
  region           = var.oci_region
}

'''
            elif provider == 'vmware':
                terraform_content += '''# VMware vSphere Provider Configuration
provider "vsphere" {
  user           = var.vmware_user
  password       = var.vmware_password
  vsphere_server = var.vmware_server

  allow_unverified_ssl = var.vmware_allow_unverified_ssl
}

'''
            elif provider == 'alibaba':
                terraform_content += '''# Alibaba Cloud Provider Configuration
provider "alicloud" {
  access_key = var.alibaba_access_key
  secret_key = var.alibaba_secret_key
  region     = var.alibaba_region
}

'''
            elif provider == 'rhcs':
                terraform_content += '''# Red Hat Cloud Services Provider Configuration
provider "rhcs" {
  token = var.rhcs_token
  url   = var.rhcs_url
}

'''
            elif provider == 'cnv':
                terraform_content += '''# CNV/Kubernetes Provider Configuration
# Uses OpenShift environment variables for authentication
provider "kubernetes" {
  host                   = var.openshift_cluster_url
  token                  = var.openshift_cluster_token
  insecure               = false
  cluster_ca_certificate = base64decode(var.openshift_cluster_ca_cert)
}

provider "kubectl" {
  host                   = var.openshift_cluster_url
  token                  = var.openshift_cluster_token
  insecure               = false
  cluster_ca_certificate = base64decode(var.openshift_cluster_ca_cert)
  load_config_file       = false
}

provider "helm" {
  host                   = var.openshift_cluster_url
  token                  = var.openshift_cluster_token
  insecure               = false
  cluster_ca_certificate = base64decode(var.openshift_cluster_ca_cert)
}

'''

        # Generate GCP project management if GCP is used
        if 'gcp' in required_providers:
            # Always create new project for GCP deployments (enables folder-based project creation)
            terraform_content += self.gcp_provider.generate_project_management(yaml_data)

        # Collect zone information for IBM VPC instances (do this once)
        ibm_vpc_zones = self.collect_ibm_vpc_zones(yaml_data)
        
        # Generate regional networking infrastructure
        terraform_content += '''# ========================================
# Regional Networking Infrastructure
# ========================================

'''
        terraform_content += self.generate_regional_networking(yaml_data, ibm_vpc_zones)

        # Generate regional security groups
        terraform_content += '''
# ========================================
# Regional Security Groups
# ========================================

'''
        terraform_content += self.generate_regional_security_groups(yaml_data)

        # Generate VM instances
        terraform_content += '''
# ========================================
# Virtual Machine Instances
# ========================================

'''
        instances = yaml_data.get('instances', [])
        
        # Validate for duplicate instance names within same provider
        self.validate_instance_names(instances)
        
        # Validate meta provider configuration
        self.validate_meta_provider_configuration(instances)
        
        # Validate IBM Cloud region consistency
        self.validate_ibm_cloud_region_consistency(instances)
        
        instance_counter = 1
        for instance in instances:
            # Get count for this instance (default to 1 if not specified)
            instance_count = instance.get('count', 1)
            
            # Generate multiple instances if count > 1
            for instance_index in range(instance_count):
                # Get zone for IBM VPC instances
                zone = None
                if instance.get('provider') == 'ibm_vpc':
                    region = self.resolve_instance_region(instance, 'ibm_vpc')
                    zone = ibm_vpc_zones.get(region)
                
                # Create a copy of the instance with modified name if count > 1
                instance_copy = instance.copy()
                if instance_count > 1:
                    original_name = instance_copy['name']
                    # Resolve GUID first before applying count naming logic
                    if '{guid}' in original_name:
                        guid = self.get_validated_guid()
                        original_name = original_name.replace('{guid}', guid)
                    
                    # Check if name ends with a number and increment from there
                    import re
                    match = re.search(r'(.+?)(\d+)$', original_name)
                    if match:
                        # Name ends with number, increment from that base
                        base_name = match.group(1)
                        base_number_str = match.group(2)
                        base_number = int(base_number_str)
                        # Preserve padding (e.g., 01, 02, 003) by using the same width
                        padding_width = len(base_number_str)
                        new_number = base_number + instance_index
                        instance_copy['name'] = f"{base_name}{new_number:0{padding_width}d}"
                    else:
                        # Name doesn't end with number, append -X
                        instance_copy['name'] = f"{original_name}-{instance_index + 1}"
                
                terraform_content += self.generate_virtual_machine(instance_copy, instance_counter, yaml_data, full_yaml_data=effective_yaml_data, zone=zone)
                instance_counter += 1

        # ROSA clusters use ROSA CLI instead of Terraform providers

        # Generate OpenShift clusters
        terraform_content += '''
# ========================================
# OpenShift Clusters
# ========================================

'''
        terraform_content += self.openshift_provider.generate_openshift_clusters(yaml_data)

        # Generate comprehensive outputs for all cloud providers
        terraform_content += '''
# ========================================
# COMPREHENSIVE OUTPUTS - ALL CLOUD PROVIDERS
# ========================================

'''
        terraform_content += self.generate_comprehensive_outputs(yaml_data, required_providers)

        return terraform_content

    def generate_comprehensive_outputs(self, yaml_data, required_providers):
        """Generate comprehensive outputs showing external IPs for all cloud providers."""
        if not yaml_data or not yaml_data.get('instances'):
            return "# No instances configured for output generation\n"
        
        outputs_content = ""
        instances = yaml_data.get('instances', [])
        guid = self.get_validated_guid(yaml_data)
        
        # Group instances by provider
        provider_instances = {}
        for instance in instances:
            provider = instance.get('provider')
            
            # Resolve meta providers to actual providers
            if provider == 'cheapest':
                provider = self.find_cheapest_provider(instance, suppress_output=True)
            elif provider == 'cheapest-gpu':
                provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
            
            if provider not in provider_instances:
                provider_instances[provider] = []
            provider_instances[provider].append(instance)
        
        # Generate outputs for each provider
        if 'aws' in provider_instances:
            outputs_content += self.generate_aws_outputs(provider_instances['aws'], yaml_data)
        
        if 'gcp' in provider_instances:
            outputs_content += self.generate_gcp_outputs(provider_instances['gcp'], yaml_data)
            
        if 'azure' in provider_instances:
            outputs_content += self.generate_azure_outputs(provider_instances['azure'], yaml_data)
            
        if 'oci' in provider_instances:
            outputs_content += self.generate_oci_outputs(provider_instances['oci'], yaml_data)
            
        if 'alibaba' in provider_instances:
            outputs_content += self.generate_alibaba_outputs(provider_instances['alibaba'], yaml_data)
            
        if 'ibm_vpc' in provider_instances:
            outputs_content += self.generate_ibm_vpc_outputs(provider_instances['ibm_vpc'], yaml_data)
            
        if 'ibm_classic' in provider_instances:
            outputs_content += self.generate_ibm_classic_outputs(provider_instances['ibm_classic'], yaml_data)
            
        if 'vmware' in provider_instances:
            outputs_content += self.generate_vmware_outputs(provider_instances['vmware'], yaml_data)
        
        # Generate summary output with all instances
        outputs_content += self.generate_summary_outputs(provider_instances, yaml_data)
        
        return outputs_content

    def generate_aws_outputs(self, instances, yaml_data):
        """Generate AWS-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# AWS Instances and External IPs
output "aws_instances" {
  description = "AWS EC2 instances with their external IPs and connection information"
  value = {''')
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            # Replace {guid} placeholder in instance name
            instance_name = self.replace_guid_placeholders(instance_name)
            clean_name, has_guid_placeholder = self.clean_name(instance_name)
            ssh_username = self.get_instance_ssh_username(instance, 'aws', yaml_data)
            
            # Check if the clean_name already ends with the GUID (to avoid duplication)
            if clean_name.endswith(f"_{guid}"):
                resource_name = clean_name
            elif has_guid_placeholder:
                resource_name = clean_name
            else:
                resource_name = f"{clean_name}_{guid}"
            
            outputs.append(f'''    "{instance_name}" = {{
      public_ip = aws_instance.{resource_name}.public_ip
      private_ip = aws_instance.{resource_name}.private_ip
      public_dns = aws_instance.{resource_name}.public_dns
      instance_id = aws_instance.{resource_name}.id
      instance_type = aws_instance.{resource_name}.instance_type
      availability_zone = aws_instance.{resource_name}.availability_zone
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{aws_instance.{resource_name}.public_ip}}"
    }}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_gcp_outputs(self, instances, yaml_data):
        """Generate GCP-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# GCP Instances and External IPs
output "gcp_instances" {
  description = "GCP Compute instances with their external IPs and connection information"
  value = {''')
        
        # Get DNS configuration to check if DNS records exist
        dns_config = yaml_data.get('dns_config', {})
        dns_enabled = dns_config.get('root_zone_management', False)
        root_zone_domain = self.gcp_provider.get_root_zone_domain(yaml_data) if dns_enabled else None
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            clean_name, has_guid_placeholder = self.clean_name(instance_name)
            
            # Use clean_name directly if GUID is already present, otherwise add GUID
            resource_name = clean_name if has_guid_placeholder else f"{clean_name}_{guid}"
            
            ssh_username = self.get_instance_ssh_username(instance, 'gcp', yaml_data)
            
            # Build the output with conditional DNS information
            output_block = f'''    "{instance_name}" = {{
      public_ip = google_compute_address.{resource_name}_ip.address
      private_ip = google_compute_instance.{resource_name}.network_interface[0].network_ip
      machine_type = google_compute_instance.{resource_name}.machine_type
      zone = google_compute_instance.{resource_name}.zone
      self_link = google_compute_instance.{resource_name}.self_link
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{google_compute_address.{resource_name}_ip.address}}"'''
            
            # Add DNS information if DNS is enabled
            if dns_enabled and root_zone_domain:
                output_block += f'''
      public_fqdn = "{instance_name}.{guid}.{root_zone_domain}"
      private_fqdn = "{instance_name}-internal.{guid}.{root_zone_domain}"
      ssh_command_fqdn = "ssh {ssh_username}@{instance_name}.{guid}.{root_zone_domain}"'''
            
            output_block += '''
    }'''
            
            outputs.append(output_block)
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_azure_outputs(self, instances, yaml_data):
        """Generate Azure-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# Azure Instances and External IPs
output "azure_instances" {
  description = "Azure Virtual Machines with their external IPs and connection information"
  value = {''')
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            clean_name, has_guid_placeholder = self.clean_name(instance_name)
            
            # Use clean_name directly if GUID is already present, otherwise add GUID
            resource_name = clean_name if has_guid_placeholder else f"{clean_name}_{guid}"
            
            ssh_username = self.get_instance_ssh_username(instance, 'azure', yaml_data)
            
            outputs.append(f'''    "{instance_name}" = {{
      public_ip = azurerm_public_ip.{resource_name}_ip.ip_address
      private_ip = azurerm_network_interface.{resource_name}_nic.ip_configuration[0].private_ip_address
      vm_size = azurerm_linux_virtual_machine.{resource_name}.size
      location = azurerm_linux_virtual_machine.{resource_name}.location
      resource_group = azurerm_linux_virtual_machine.{resource_name}.resource_group_name
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{azurerm_public_ip.{resource_name}_ip.ip_address}}"
    }}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_oci_outputs(self, instances, yaml_data):
        """Generate OCI-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# OCI Instances and External IPs
output "oci_instances" {
  description = "OCI Compute instances with their external IPs and connection information"
  value = {''')
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            clean_name = self.clean_name(instance_name)
            ssh_username = self.get_instance_ssh_username(instance, 'oci', yaml_data)
            
            outputs.append(f'''    "{instance_name}" = {{
      public_ip = oci_core_instance.{clean_name}_{guid}.public_ip
      private_ip = oci_core_instance.{clean_name}_{guid}.private_ip
      shape = oci_core_instance.{clean_name}_{guid}.shape
      availability_domain = oci_core_instance.{clean_name}_{guid}.availability_domain
      compartment_id = oci_core_instance.{clean_name}_{guid}.compartment_id
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{oci_core_instance.{clean_name}_{guid}.public_ip}}"
    }}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_alibaba_outputs(self, instances, yaml_data):
        """Generate Alibaba Cloud-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# Alibaba Cloud Instances and External IPs
output "alibaba_instances" {
  description = "Alibaba Cloud ECS instances with their external IPs and connection information"
  value = {''')
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            clean_name = self.clean_name(instance_name)
            ssh_username = self.get_instance_ssh_username(instance, 'alibaba', yaml_data)
            
            outputs.append(f'''    "{instance_name}" = {{
      public_ip = alicloud_eip_association.{clean_name}_eip_assoc_{guid}.ip_address
      private_ip = alicloud_instance.{clean_name}_{guid}.private_ip
      instance_type = alicloud_instance.{clean_name}_{guid}.instance_type
      zone_id = alicloud_instance.{clean_name}_{guid}.availability_zone
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{alicloud_eip_association.{clean_name}_eip_assoc_{guid}.ip_address}}"
    }}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_ibm_vpc_outputs(self, instances, yaml_data):
        """Generate IBM VPC-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# IBM Cloud VPC Instances and External IPs
output "ibm_vpc_instances" {
  description = "IBM Cloud VPC instances with their external IPs and connection information"
  value = {''')
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            clean_name, has_guid_placeholder = self.clean_name(instance_name)
            ssh_username = self.get_instance_ssh_username(instance, 'ibm_vpc', yaml_data)
            
            # Use clean_name directly if GUID is already present, otherwise add GUID
            resource_name = clean_name if has_guid_placeholder else f"{clean_name}_{guid}"
            
            outputs.append(f'''    "{instance_name}" = {{
      public_ip = ibm_is_floating_ip.{resource_name}_fip.address
      private_ip = ibm_is_instance.{resource_name}.primary_network_interface[0].primary_ip
      profile = ibm_is_instance.{resource_name}.profile
      zone = ibm_is_instance.{resource_name}.zone
      vpc = ibm_is_instance.{resource_name}.vpc
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{ibm_is_floating_ip.{resource_name}_fip.address}}"
    }}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_ibm_classic_outputs(self, instances, yaml_data):
        """Generate IBM Classic-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# IBM Cloud Classic Instances and External IPs
output "ibm_classic_instances" {
  description = "IBM Cloud Classic instances with their external IPs and connection information"
  value = {''')
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            clean_name, has_guid_placeholder = self.clean_name(instance_name)
            ssh_username = self.get_instance_ssh_username(instance, 'ibm_classic', yaml_data)
            
            # Use clean_name directly if GUID is already present, otherwise add GUID
            resource_name = clean_name if has_guid_placeholder else f"{clean_name}_{guid}"
            
            outputs.append(f'''    "{instance_name}" = {{
      public_ip = ibm_compute_vm_instance.{resource_name}.ipv4_address
      private_ip = ibm_compute_vm_instance.{resource_name}.ipv4_address_private
      flavor = ibm_compute_vm_instance.{resource_name}.flavor_key_name
      datacenter = ibm_compute_vm_instance.{resource_name}.datacenter
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{ibm_compute_vm_instance.{resource_name}.ipv4_address}}"
    }}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_vmware_outputs(self, instances, yaml_data):
        """Generate VMware-specific outputs."""
        if not instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# VMware vSphere Instances and IPs
output "vmware_instances" {
  description = "VMware vSphere VMs with their IP addresses and connection information"
  value = {''')
        
        for instance in instances:
            instance_name = instance.get("name", "unknown")
            clean_name, has_guid_placeholder = self.clean_name(instance_name)
            ssh_username = self.get_instance_ssh_username(instance, 'vmware', yaml_data)
            
            # Use clean_name directly if GUID is already present, otherwise add GUID
            resource_name = clean_name if has_guid_placeholder else f"{clean_name}_{guid}"
            
            outputs.append(f'''    "{instance_name}" = {{
      ip_address = vsphere_virtual_machine.{resource_name}.default_ip_address
      guest_ip_addresses = vsphere_virtual_machine.{resource_name}.guest_ip_addresses
      num_cpus = vsphere_virtual_machine.{resource_name}.num_cpus
      memory = vsphere_virtual_machine.{resource_name}.memory
      power_state = vsphere_virtual_machine.{resource_name}.power_state
      ssh_username = "{ssh_username}"
      ssh_command = "ssh {ssh_username}@${{vsphere_virtual_machine.{resource_name}.default_ip_address}}"
    }}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def generate_summary_outputs(self, provider_instances, yaml_data):
        """Generate summary output with all instances across all providers."""
        if not provider_instances:
            return ""
        
        guid = self.get_validated_guid(yaml_data)
        outputs = []
        
        outputs.append('''# MULTI-CLOUD SUMMARY - ALL INSTANCES
output "all_instances_summary" {
  description = "Summary of all instances across all cloud providers with their external IPs"
  value = {''')
        
        for provider, instances in provider_instances.items():
            for instance in instances:
                instance_name = instance.get("name", "unknown")
                # clean_name already handles GUID replacement
                clean_name, has_guid_placeholder = self.clean_name(instance_name)
                ssh_username = self.get_instance_ssh_username(instance, provider, yaml_data)
                
                # Use clean_name directly if GUID is already present, otherwise add GUID
                resource_name = clean_name if has_guid_placeholder else f"{clean_name}_{guid}"
                
                # Generate provider-specific IP reference
                if provider == 'aws':
                    ip_ref = f"aws_instance.{resource_name}.public_ip"
                    private_ip_ref = f"aws_instance.{resource_name}.private_ip"
                elif provider == 'gcp':
                    ip_ref = f"google_compute_address.{resource_name}_ip.address"
                    private_ip_ref = f"google_compute_instance.{resource_name}.network_interface[0].network_ip"
                elif provider == 'azure':
                    ip_ref = f"azurerm_public_ip.{resource_name}_ip.ip_address"
                    private_ip_ref = f"azurerm_network_interface.{resource_name}_nic.ip_configuration[0].private_ip_address"
                elif provider == 'oci':
                    ip_ref = f"oci_core_instance.{resource_name}.public_ip"
                    private_ip_ref = f"oci_core_instance.{resource_name}.private_ip"
                elif provider == 'alibaba':
                    ip_ref = f"alicloud_eip_association.{resource_name}_eip_assoc.ip_address"
                    private_ip_ref = f"alicloud_instance.{resource_name}.private_ip"
                elif provider == 'ibm_vpc':
                    ip_ref = f"ibm_is_floating_ip.{resource_name}_fip.address"
                    private_ip_ref = f"ibm_is_instance.{resource_name}.primary_network_interface[0].primary_ip"
                elif provider == 'ibm_classic':
                    ip_ref = f"ibm_compute_vm_instance.{resource_name}.ipv4_address"
                    private_ip_ref = f"ibm_compute_vm_instance.{resource_name}.ipv4_address_private"
                elif provider == 'vmware':
                    ip_ref = f"vsphere_virtual_machine.{resource_name}.default_ip_address"
                    private_ip_ref = f"vsphere_virtual_machine.{resource_name}.default_ip_address"
                elif provider == 'cnv':
                    ip_ref = "\"NODE_IP\""
                    private_ip_ref = "\"N/A\""
                else:
                    ip_ref = "\"N/A\""
                    private_ip_ref = "\"N/A\""
                
                # Generate SSH command based on provider
                if provider == 'cnv':
                    ssh_command = f"ssh {ssh_username}@${{{ip_ref}}} -p ${{kubernetes_service.{resource_name}_ssh_service.spec[0].port[0].node_port}}"
                else:
                    ssh_command = f"ssh {ssh_username}@${{{ip_ref}}}"
                
                outputs.append(f'''    "{instance_name}" = {{
      provider = "{self.format_provider_name_for_display(provider)}"
      public_ip = {ip_ref}
      private_ip = {private_ip_ref}
      size = "{instance.get('size', 'unknown')}"
      image = "{instance.get('image', 'unknown')}"
      region = "{instance.get('region', 'N/A')}"
      ssh_username = "{ssh_username}"
      ssh_command = "{ssh_command}"
    }}''')
        
        outputs.append('''  }
}

# External IPs Only - Quick Access
output "external_ips" {
  description = "Quick access to all external IP addresses"
  value = {''')
        
        for provider, instances in provider_instances.items():
            for instance in instances:
                instance_name = instance.get("name", "unknown")
                # clean_name already handles GUID replacement
                clean_name, has_guid_placeholder = self.clean_name(instance_name)
                
                # Use clean_name directly if GUID is already present, otherwise add GUID
                resource_name = clean_name if has_guid_placeholder else f"{clean_name}_{guid}"
                
                # Generate provider-specific IP reference
                if provider == 'aws':
                    ip_ref = f"aws_instance.{resource_name}.public_ip"
                elif provider == 'gcp':
                    ip_ref = f"google_compute_address.{resource_name}_ip.address"
                elif provider == 'azure':
                    ip_ref = f"azurerm_public_ip.{resource_name}_ip.ip_address"
                elif provider == 'oci':
                    ip_ref = f"oci_core_instance.{resource_name}.public_ip"
                elif provider == 'alibaba':
                    ip_ref = f"alicloud_eip_association.{resource_name}_eip_assoc.ip_address"
                elif provider == 'ibm_vpc':
                    ip_ref = f"ibm_is_floating_ip.{resource_name}_fip.address"
                elif provider == 'ibm_classic':
                    ip_ref = f"ibm_compute_vm_instance.{resource_name}.ipv4_address"
                elif provider == 'vmware':
                    ip_ref = f"vsphere_virtual_machine.{resource_name}.default_ip_address"
                elif provider == 'cnv':
                    ip_ref = "\"NODE_IP\""
                else:
                    ip_ref = "\"N/A\""
                
                outputs.append(f'''    "{instance_name}" = {ip_ref}''')
        
        outputs.append('''  }
}

''')
        return '\n'.join(outputs)

    def format_provider_name_for_display(self, provider):
        """Format provider name for display purposes."""
        provider_display_names = {
            'aws': 'AWS',
            'gcp': 'GCP', 
            'azure': 'Azure',
            'oci': 'OCI',
            'alibaba': 'Alibaba Cloud',
            'ibm_vpc': 'IBM VPC',
            'ibm_classic': 'IBM Classic',
            'vmware': 'VMware'
        }
        return provider_display_names.get(provider, provider.upper())

    def print_provider_output(self, provider, message, indent_level=1):
        """Print provider-specific output with proper formatting."""
        provider_name = self.format_provider_name_for_display(provider)
        indent = "  " * indent_level
        print(f"{indent}{provider_name}: {message}")

    def start_provider_section(self, provider):
        """Start a new provider section in output."""
        provider_name = self.format_provider_name_for_display(provider)
        print(f"{provider_name}:")

    def start_global_section(self):
        """Start the global section for setup operations."""
        if not getattr(self, 'ansible_mode', False):
            print()
            print("[GLOBAL]")

    def print_global_output(self, message, indent_level=1):
        """Print global output with proper indentation."""
        if not getattr(self, 'ansible_mode', False):
            indent = "  " * indent_level
            print(f"{indent}{message}")

    def start_instance_section(self, instance_name, provider):
        """Start a new instance section in the output."""
        print()
        # Replace {guid} with actual GUID in instance name
        resolved_instance_name = self.replace_guid_placeholders(instance_name)
        print(f"[{resolved_instance_name}]")

    def print_instance_output(self, instance_name, provider, message, indent_level=1):
        """Print instance-specific output with proper indentation."""
        indent = "  " * indent_level
        print(f"{indent}{message}")

    def _print_cost_analysis_for_instance(self, instance, selected_provider):
        """Print cost analysis for cheapest provider selection under instance section."""
        instance_name = instance.get('name', 'unnamed')
        instance_count = instance.get('count', 1)
        # Get instance-specific exclusions
        instance_exclusions = instance.get('exclude_providers', [])
        
        # Get cost information for all providers
        # Handle memory in either MB or GB format
        memory_value = instance.get('memory', 4)
        if memory_value < 100:  # Assume GB if less than 100
            memory_mb = memory_value * 1024
        else:  # Assume MB if 100 or greater
            memory_mb = memory_value
            
        provider_costs = self.find_cheapest_by_specs(
            instance.get('cores', 2), 
            memory_mb,
            instance.get('gpu_count', 0),
            instance.get('gpu_type'),
            instance_exclusions
        )
        
        if provider_costs:
            # Apply discounts to provider costs for display
            for provider in provider_costs:
                original_cost = provider_costs[provider]['cost']
                provider_costs[provider]['original_cost'] = original_cost
                provider_costs[provider]['cost'] = self.apply_discount(original_cost, provider)
            
            cost_header = f"Cost analysis (x{instance_count} instances):" if instance_count > 1 else "Cost analysis:"
            self.print_instance_output(instance_name, selected_provider, cost_header)
            for provider, info in sorted(provider_costs.items(), key=lambda x: x[1]['cost']):
                marker = " ← SELECTED" if provider == selected_provider else ""
                vcpus = info.get('vcpus', 'N/A')
                memory_gb = info.get('memory_gb', 'N/A')
                gpu_count = info.get('gpu_count', 0)
                detected_gpu_type = info.get('gpu_type', '')
                
                # Calculate total cost for all instances (using discounted cost)
                per_hour_cost = info['cost']
                total_hourly_cost = per_hour_cost * instance_count
                
                # Format cost with discount info
                cost_display = self._format_cost_with_discount(provider, info.get('original_cost'), per_hour_cost)
                
                if gpu_count > 0:
                    gpu_info = f", {gpu_count}x {detected_gpu_type}" if detected_gpu_type else f", {gpu_count} GPU(s)"
                    if instance_count > 1:
                        self.print_instance_output(instance_name, selected_provider, 
                            f"  {provider}: {cost_display} each, ${total_hourly_cost:.4f}/hour total ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB{gpu_info}){marker}", 2)
                    else:
                        self.print_instance_output(instance_name, selected_provider, 
                            f"  {provider}: {cost_display} ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB{gpu_info}){marker}", 2)
                else:
                    if instance_count > 1:
                        self.print_instance_output(instance_name, selected_provider, 
                            f"  {provider}: {cost_display} each, ${total_hourly_cost:.4f}/hour total ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB){marker}", 2)
                    else:
                        self.print_instance_output(instance_name, selected_provider, 
                            f"  {provider}: {cost_display} ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB){marker}", 2)

    def _print_gpu_cost_analysis_for_instance(self, instance, selected_provider):
        """Print GPU cost analysis for cheapest GPU provider selection under instance section."""
        instance_name = instance.get('name', 'unnamed')
        instance_count = instance.get('count', 1)
        gpu_type = instance.get("gpu_type")
        
        # Get instance-specific exclusions
        instance_exclusions = instance.get('exclude_providers', [])
        
        # Get cost information for all providers (GPU instances only)
        provider_costs = self.find_cheapest_gpu_by_specs(gpu_type, instance_exclusions)
        
        if provider_costs:
            # Apply discounts to provider costs for display
            for provider in provider_costs:
                original_cost = provider_costs[provider]['cost']
                provider_costs[provider]['original_cost'] = original_cost
                provider_costs[provider]['cost'] = self.apply_discount(original_cost, provider)
            
            analysis_type = f"cheapest {gpu_type} GPU" if gpu_type else "cheapest GPU (any type)"
            cost_header = f"GPU-optimized cost analysis ({analysis_type}, x{instance_count} instances):" if instance_count > 1 else f"GPU-optimized cost analysis ({analysis_type}):"
            self.print_instance_output(instance_name, selected_provider, cost_header)
            for provider, info in sorted(provider_costs.items(), key=lambda x: x[1]['cost']):
                marker = " ← SELECTED" if provider == selected_provider else ""
                vcpus = info.get('vcpus', 'N/A')
                memory_gb = info.get('memory_gb', 'N/A')
                gpu_count = info.get('gpu_count', 0)
                detected_gpu_type = info.get('gpu_type', '')
                
                # Calculate total cost for all instances (using discounted cost)
                per_hour_cost = info['cost']
                total_hourly_cost = per_hour_cost * instance_count
                
                # Format cost with discount info
                cost_display = self._format_cost_with_discount(provider, info.get('original_cost'), per_hour_cost)
                
                gpu_info = f", {gpu_count}x {detected_gpu_type}" if detected_gpu_type else f", {gpu_count} GPU(s)"
                if instance_count > 1:
                    self.print_instance_output(instance_name, selected_provider, 
                        f"  {provider}: {cost_display} each, ${total_hourly_cost:.4f}/hour total ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB{gpu_info}){marker}", 2)
                else:
                    self.print_instance_output(instance_name, selected_provider, 
                        f"  {provider}: {cost_display} ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB{gpu_info}){marker}", 2)

    def get_instance_ssh_username(self, instance, provider, yaml_data=None):
        """Get the SSH username for an instance based on provider and operating system."""
        # Check if username is explicitly configured for this instance
        if instance.get('username'):
            return instance['username']
        
        # Check if SSH username is explicitly configured for this instance via SSH key config
        ssh_key_config = self.get_instance_ssh_key(instance, yaml_data or {})
        if ssh_key_config and ssh_key_config.get('username'):
            return ssh_key_config['username']
        
        # Get the default username from core configuration
        default_username = self.core_config.get('security', {}).get('default_username', 'cloud-user')
        
        # Determine username based on provider and image
        image = instance.get('image', '')
        
        # Debug output to trace the issue  
        # print(f"SSH DEBUG START: provider='{provider}', image='{image}'")
        
        if provider == 'aws':
            # AWS usernames - use configurable default for all images
            if any(os_name in image.upper() for os_name in ['RHEL', 'REDHAT']):
                return default_username
            elif 'UBUNTU' in image.upper():
                return default_username
            elif any(os_name in image.upper() for os_name in ['AMAZON', 'AMZN']):
                return default_username
            elif 'CENTOS' in image.upper():
                return default_username
            elif 'FEDORA' in image.upper():
                return default_username
            elif 'SUSE' in image.upper():
                return default_username
            else:
                return default_username  # Default for AWS
                
        elif provider == 'gcp':
            # GCP usernames - use configurable default for all images
            if any(os_name in image.upper() for os_name in ['RHEL', 'REDHAT']):
                return default_username
            elif 'UBUNTU' in image.upper():
                return default_username
            elif 'CENTOS' in image.upper():
                return default_username
            elif 'FEDORA' in image.upper():
                return default_username
            elif 'DEBIAN' in image.upper():
                return default_username
            else:
                return default_username  # Default for GCP
                
        elif provider == 'azure':
            # Azure usernames - use configurable default
            return default_username
            
        elif provider == 'oci':
            # OCI usernames - use configurable default for all images
            if any(os_name in image.upper() for os_name in ['ORACLE', 'OL']):
                return default_username
            elif 'UBUNTU' in image.upper():
                return default_username
            elif any(os_name in image.upper() for os_name in ['RHEL', 'REDHAT']):
                return default_username
            elif 'CENTOS' in image.upper():
                return default_username
            else:
                return default_username  # Default for OCI
                
        elif provider == 'alibaba':
            # Alibaba Cloud - use configurable default
            return default_username
            
        elif provider in ['ibm_vpc', 'ibm_classic']:
            # IBM Cloud username depends on RHEL version and cloud-user configuration
            if any(os_name in image.upper() for os_name in ['RHEL', 'REDHAT']):
                # Check if cloud-user creation is enabled in IBM VPC config
                ibm_vpc_config = yaml_data.get('yamlforge', {}).get('ibm_vpc', {})
                create_cloud_user = ibm_vpc_config.get('create_cloud_user', True)  # Default to True
                
                if create_cloud_user:
                    # Check for RHEL version
                    import re
                    version_match = re.search(r'(\d+)', image)
                    if version_match:
                        version = version_match.group(1)
                        if version == '9':
                            return default_username  # RHEL 9 uses configurable default
                        else:
                            return default_username  # RHEL 8 and earlier also use configurable default
                    else:
                        return default_username  # Default to configurable default if version can't be determined
                else:
                    return 'root'  # Use root when cloud-user creation is disabled
            else:
                return default_username  # Default for non-RHEL images
            
        elif provider == 'vmware':
            # VMware - use configurable default
            return default_username
            
        else:
            # Unknown provider - return configurable default
            return default_username

    def generate_variables_tf(self, required_providers, yaml_data=None):
        """Generate variables.tf file with variables for required providers."""
        variables_content = '''# Variables for multi-cloud deployment
# Generated by YamlForge v2.0
# SSH keys are configured in YAML and embedded directly in resources

'''

        if 'aws' in required_providers:
            # Get the primary AWS region that will be used
            primary_aws_region = self.get_primary_aws_region(yaml_data) if yaml_data else None
            
            variables_content += f'''# AWS Variables
# Note: AWS region is determined from your YAML configuration: {primary_aws_region or 'No regions specified'}

variable "aws_billing_account_id" {{
  description = "AWS billing account ID for ROSA clusters (overrides default account)"
  type        = string
  default     = ""
}}

# ROSA clusters use ROSA CLI for authentication and cluster creation
# AWS credentials are configured via environment variables or AWS profiles
# AWS billing account can be overridden via AWS_BILLING_ACCOUNT_ID environment variable

'''

        if 'azure' in required_providers:
            variables_content += '''# Azure Variables
variable "azure_subscription_id" {
  description = "Azure subscription ID"
  type        = string
}

variable "azure_location" {
  description = "Azure location for deployment"
  type        = string
  default     = "East US"
}

variable "arm_client_secret" {
  description = "Azure service principal client secret for ARO clusters"
  type        = string
  sensitive   = true
}

'''

        if 'gcp' in required_providers:
            # Always creating new project - only need region variable
            variables_content += '''# GCP Variables
variable "gcp_region" {
  description = "GCP region for deployment"
  type        = string
  default     = "us-east1"
}

# Note: GCP project is created automatically using cloud_workspace name and GUID

'''

        if 'ibm_vpc' in required_providers or 'ibm_classic' in required_providers:
            variables_content += '''# IBM Cloud Variables
variable "ibm_api_key" {
  description = "IBM Cloud API key for authentication"
  type        = string
  sensitive   = true
}

variable "ibm_region" {
  description = "IBM Cloud region for deployment"
  type        = string
  default     = "us-south"
}

'''

        if 'oci' in required_providers:
            variables_content += '''# Oracle Cloud Infrastructure Variables
variable "oci_tenancy_ocid" {
  description = "OCI tenancy OCID"
  type        = string
}

variable "oci_user_ocid" {
  description = "OCI user OCID"
  type        = string
}

variable "oci_fingerprint" {
  description = "OCI API key fingerprint"
  type        = string
}

variable "oci_private_key_path" {
  description = "Path to OCI private key file"
  type        = string
}

variable "oci_region" {
  description = "OCI region for deployment"
  type        = string
  default     = "us-ashburn-1"
}

variable "oci_compartment_id" {
  description = "OCI compartment OCID"
  type        = string
}

'''

        if 'vmware' in required_providers:
            variables_content += '''# VMware vSphere Variables
variable "vmware_user" {
  description = "VMware vSphere username"
  type        = string
}

variable "vmware_password" {
  description = "VMware vSphere password"
  type        = string
  sensitive   = true
}

variable "vmware_server" {
  description = "VMware vSphere server hostname or IP"
  type        = string
}

variable "vmware_datacenter" {
  description = "VMware vSphere datacenter name"
  type        = string
  default     = "Datacenter"
}

variable "vmware_cluster" {
  description = "VMware vSphere cluster name"
  type        = string
  default     = "Cluster"
}

variable "vmware_datastore" {
  description = "VMware vSphere datastore name"
  type        = string
  default     = "datastore1"
}

variable "vmware_network" {
  description = "VMware vSphere network name"
  type        = string
  default     = "VM Network"
}

variable "vmware_allow_unverified_ssl" {
  description = "Allow unverified SSL certificates for vSphere"
  type        = bool
  default     = true
}

'''

        if 'alibaba' in required_providers:
            variables_content += '''# Alibaba Cloud Variables
variable "alibaba_access_key" {
  description = "Alibaba Cloud access key"
  type        = string
  sensitive   = true
}

variable "alibaba_secret_key" {
  description = "Alibaba Cloud secret key"
  type        = string
  sensitive   = true
}

variable "alibaba_region" {
  description = "Alibaba Cloud region for deployment"
  type        = string
  default     = "cn-hangzhou"
}

'''

        if 'rhcs' in required_providers:
            variables_content += '''# Red Hat Cloud Services Variables
variable "rhcs_token" {
  description = "Red Hat OpenShift Cluster Manager offline token"
  type        = string
  sensitive   = true
}

variable "rhcs_url" {
  description = "Red Hat OpenShift Cluster Manager URL"
  type        = string
  default     = "https://api.openshift.com"
}

variable "redhat_openshift_api_url" {
  description = "Red Hat OpenShift API URL"
  type        = string
  default     = "https://api.openshift.com"
}

'''
        if 'cnv' in required_providers:
            variables_content += '''# CNV/Kubernetes Variables
variable "openshift_cluster_url" {
  description = "OpenShift cluster API URL"
  type        = string
}

variable "openshift_cluster_token" {
  description = "OpenShift cluster authentication token"
  type        = string
  sensitive   = true
}

variable "openshift_cluster_ca_cert" {
  description = "OpenShift cluster CA certificate (base64 encoded)"
  type        = string
  sensitive   = true
}

variable "no_credentials_mode" {
  description = "Run in no-credentials mode (skip dynamic lookups)"
  type        = bool
  default     = false
}

'''

                # Add OpenShift variables if clusters are present
        if yaml_data:
            openshift_clusters = yaml_data.get('openshift_clusters', [])
            if openshift_clusters:
                variables_content += self.openshift_provider.generate_openshift_variables(openshift_clusters)

        # Add common infrastructure variables
        variables_content += '''# =============================================================================
# COMMON INFRASTRUCTURE VARIABLES
# =============================================================================

variable "ssh_public_key" {
  description = "SSH public key for instance access"
  type        = string
}

variable "key_name" {
  description = "Name of the SSH key pair"
  type        = string
  default     = "default-key"
}

variable "environment" {
  description = "Environment tag for resources"
  type        = string
  default     = "development"
}

variable "common_tags" {
  description = "Common tags to apply to all resources"
  type        = map(string)
  default     = {}
}

'''

        return variables_content

    def generate_terraform_tfvars(self, required_providers, yaml_data=None):
        """Generate terraform.tfvars configuration file with auto-discovered values."""
        # Get default SSH key
        ssh_key_info = self.credentials.get_default_ssh_key()
        
        tfvars_content = '''# Terraform Variables Configuration
# Copy this file to terraform.tfvars and customize with your values
# Generated by YamlForge v2.0

'''
        
        # Add SSH key section
        if ssh_key_info.get('available'):
            # Mask the SSH key for security (show first 20 and last 10 characters)
            ssh_key = ssh_key_info.get('public_key', '')

                
            tfvars_content += f'''# SSH public key for instance access
# Automatically detected from: {ssh_key_info.get('source')}
ssh_public_key = "{ssh_key}"

'''
        else:
            tfvars_content += '''# SSH public key for instance access (required)
# Set SSH_PUBLIC_KEY environment variable or add to defaults/core.yaml
# Examples:
#   export SSH_PUBLIC_KEY="$(cat ~/.ssh/id_rsa.pub)"
#   export SSH_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAA... user@host"
ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAA... your-public-key-here"

'''

        if 'aws' in required_providers:
            # Get AWS credentials with auto-discovery (skip in no-credentials mode)
            if self.no_credentials:
                aws_creds = {'available': False}
                print("  NO-CREDENTIALS MODE: Skipping AWS credential discovery")
            else:
                aws_creds = self.credentials.get_aws_credentials()
            
            tfvars_content += '''# =============================================================================
# AWS CONFIGURATION
# =============================================================================

'''
            
            # Try to auto-detect AWS credentials from environment variables
            aws_billing_account_id = os.getenv('AWS_BILLING_ACCOUNT_ID')
            
            # Get the determined AWS region from YAML only
            determined_region = self.get_primary_aws_region(yaml_data) if yaml_data else None
            
            # ROSA CLI uses AWS credentials from environment variables or AWS CLI profiles
            tfvars_content += f'''# AWS Region Configuration
# Region determined from your YAML configuration: {determined_region or 'No regions specified'}

# AWS Billing Account Override (if different from default account)
aws_billing_account_id = "{aws_billing_account_id or ''}"

# ROSA CLI uses AWS credentials from environment variables or AWS CLI profiles
# No need to set aws_access_key_id or aws_secret_access_key in Terraform

'''
            
            # AWS account information - try environment variables first, then AWS SDK
            account_id_from_env = None
            billing_account_id = aws_billing_account_id
            
            if aws_creds.get('available'):
                # Use auto-discovered values from AWS SDK
                account_id_from_env = aws_creds.get('account_id')
                # Use environment variable for billing account ID if set, otherwise use account ID
                billing_account_id = billing_account_id or account_id_from_env
                
                tfvars_content += f'''# AWS Account Information (auto-detected from AWS credentials)
# ROSA clusters use ROSA CLI for authentication and cluster creation
# aws_account_id        = "{account_id_from_env}"
# aws_billing_account_id = "{billing_account_id}"

'''
            else:
                # Fall back to environment variables if AWS SDK not available
                if billing_account_id:
                    tfvars_content += f'''# AWS Account Information (partially from environment variables)
# aws_account_id        = "your-account-id"  # AWS SDK not available
# Official Red Hat modules automatically handle AWS account information
# aws_rosa_creator_arn  = "arn:aws:iam::account:user/your-username"

# ROSA clusters use ROSA CLI for authentication (no manual role configuration needed)
# aws_rosa_installer_role_arn = "arn:aws:iam::your-account-id:role/ManagedOpenShift-Installer-Role"
# aws_rosa_support_role_arn   = "arn:aws:iam::your-account-id:role/ManagedOpenShift-Support-Role"
# aws_rosa_worker_role_arn    = "arn:aws:iam::your-account-id:role/ManagedOpenShift-Worker-Role"
# aws_rosa_master_role_arn    = "arn:aws:iam::your-account-id:role/ManagedOpenShift-ControlPlane-Role"

'''
                else:
                    tfvars_content += '''# AWS Account Information for ROSA Clusters
# ROSA clusters use ROSA CLI for authentication and cluster creation
# AWS credentials configured via environment variables or AWS CLI profiles

'''

        if 'azure' in required_providers:
            if self.no_credentials:
                # In no-credentials mode, use placeholders for Azure
                subscription_id = "placeholder-subscription-id"
                arm_client_secret = "placeholder-client-secret"
            else:
                # Get Azure credentials from environment variables (no defaults)
                azure_creds = self.credentials.get_azure_credentials()
                subscription_id = azure_creds.get('subscription_id')
                
                if not subscription_id:
                    raise ValueError("Azure subscription ID not found. Please set ARM_SUBSCRIPTION_ID or AZURE_SUBSCRIPTION_ID environment variable.")
                
                # Get ARM_CLIENT_SECRET for ARO service principal
                arm_client_secret = azure_creds.get('client_secret')
                
                if not arm_client_secret:
                    raise ValueError("Azure client secret not found. Please set ARM_CLIENT_SECRET environment variable.")
                
            tfvars_content += f'''# Azure Configuration (from environment variables)
azure_subscription_id = "{subscription_id}"
azure_location        = "East US"
arm_client_secret     = "{arm_client_secret}"

'''

        if 'gcp' in required_providers:
            # Always creating new project
            yamlforge_data = yaml_data.get('yamlforge', {}) if yaml_data else {}
            cloud_workspace = yamlforge_data.get('cloud_workspace', {})
            workspace_name = cloud_workspace.get('name', 'yamlforge-workspace')
            tfvars_content += f'''# GCP Configuration
# Project will be created automatically as: {workspace_name.lower().replace('_', '-')}-{{GUID}}
# Folder ID: {os.getenv('GCP_FOLDER_ID', 'Not set - will use organization')}
gcp_region = "us-east1"

'''

        if 'ibm_vpc' in required_providers or 'ibm_classic' in required_providers:
            # Check for IBM Cloud API key from environment variables
            ibm_api_key = os.getenv('IC_API_KEY') or os.getenv('IBMCLOUD_API_KEY')
            
            # Determine IBM VPC region from YAML configuration
            ibm_region = "us-south"  # Default fallback
            if yaml_data and 'yamlforge' in yaml_data:
                ibm_vpc_config = yaml_data['yamlforge'].get('ibm_vpc', {})
                if 'region' in ibm_vpc_config:
                    ibm_region = ibm_vpc_config['region']
                else:
                    self.print_provider_output('ibm_vpc', f"No region specified in YAML, using default: {ibm_region}")
            
            if ibm_api_key:
                tfvars_content += f'''# IBM Cloud Configuration
# API key automatically detected from environment variable
ibm_api_key = "{ibm_api_key}"
ibm_region = "{ibm_region}"

'''
            else:
                tfvars_content += f'''# IBM Cloud Configuration
# Set IC_API_KEY or IBMCLOUD_API_KEY environment variable
# Or configure ibm_api_key here
ibm_api_key = "your-ibm-cloud-api-key-here"
ibm_region = "{ibm_region}"

'''

        if 'oci' in required_providers:
            tfvars_content += '''# Oracle Cloud Infrastructure Configuration
oci_tenancy_ocid     = "ocid1.tenancy.oc1..aaaaaaaaa..."
oci_user_ocid        = "ocid1.user.oc1..aaaaaaaaa..."
oci_fingerprint      = "aa:bb:cc:dd:ee:ff:gg:hh:ii:jj:kk:ll:mm:nn:oo:pp"
oci_private_key_path = "~/.oci/oci_api_key.pem"
oci_region           = "us-ashburn-1"
oci_compartment_id   = "ocid1.compartment.oc1..aaaaaaaaa..."

'''

        if 'vmware' in required_providers:
            tfvars_content += '''# VMware vSphere Configuration
vmware_user                   = "administrator@vsphere.local"
vmware_password              = "your-password-here"
vmware_server                = "vcenter.example.com"
vmware_datacenter            = "Datacenter"
vmware_cluster               = "Cluster"
vmware_datastore             = "datastore1"
vmware_network               = "VM Network"
vmware_allow_unverified_ssl  = true

'''

        if 'alibaba' in required_providers:
            tfvars_content += '''# Alibaba Cloud Configuration
alibaba_access_key = "LTAI4G..."
alibaba_secret_key = "your-secret-key-here"
alibaba_region     = "cn-hangzhou"

'''

        if 'rhcs' in required_providers:
            # Check for Red Hat OpenShift token
            rhcs_token = os.getenv('REDHAT_OPENSHIFT_TOKEN')
            
            tfvars_content += '''# =============================================================================
# RED HAT CLOUD SERVICES CONFIGURATION
# =============================================================================

'''
            
            if rhcs_token:
                # Get Red Hat API URL from environment variable
                rhcs_url = os.getenv('REDHAT_OPENSHIFT_API_URL', 'https://api.openshift.com')
                
                tfvars_content += f'''# Red Hat OpenShift Cluster Manager Token
# Automatically detected from environment variable
rhcs_token = "{rhcs_token}"
rhcs_url   = "{rhcs_url}"

'''
            else:
                # Get Red Hat API URL from environment variable
                rhcs_url = os.getenv('REDHAT_OPENSHIFT_API_URL', 'https://api.openshift.com')
                
                tfvars_content += f'''# Red Hat OpenShift Cluster Manager Token (required for ROSA)
# Get your token from: https://console.redhat.com/openshift/token/rosa
# Set REDHAT_OPENSHIFT_TOKEN environment variable or configure here
rhcs_token = "your-offline-token-here"
rhcs_url   = "{rhcs_url}"

'''

        # Red Hat Pull Secret for enhanced content access (only for OpenShift clusters)
        if self._has_rosa_clusters(yaml_data):
            pull_secret = os.getenv('OCP_PULL_SECRET')
            
            if pull_secret and pull_secret.strip():
                # Escape the JSON for use in tfvars (single line, escaped quotes)
                escaped_pull_secret = pull_secret.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '')
                tfvars_content += f'''# Red Hat Pull Secret for enhanced content access
# Automatically detected from OCP_PULL_SECRET environment variable
# This enables access to Red Hat container registries and additional content
redhat_pull_secret = "{escaped_pull_secret}"

'''
            else:
                tfvars_content += '''# Red Hat Pull Secret for enhanced content access (optional but recommended)
# Download from: https://console.redhat.com/openshift/install/pull-secret
# Set OCP_PULL_SECRET environment variable or configure here
redhat_pull_secret = ""

'''

        # CNV/Kubernetes configuration
        if 'cnv' in required_providers:
            # Get OpenShift cluster credentials from environment variables
            openshift_cluster_url = os.getenv('OPENSHIFT_CLUSTER_URL')
            openshift_cluster_token = os.getenv('OPENSHIFT_CLUSTER_TOKEN')
            
            if openshift_cluster_url and openshift_cluster_token:
                tfvars_content += f'''# =============================================================================
# CNV/KUBERNETES CONFIGURATION
# =============================================================================

# OpenShift Cluster Configuration
# Automatically detected from environment variables
openshift_cluster_url = "{openshift_cluster_url}"
openshift_cluster_token = "{openshift_cluster_token}"

# Note: openshift_cluster_ca_cert is optional for most OpenShift clusters
# Set OPENSHIFT_CLUSTER_CA_CERT environment variable if needed
openshift_cluster_ca_cert = ""
no_credentials_mode = {str(self.no_credentials).lower()}

'''
            else:
                tfvars_content += '''# =============================================================================
# CNV/KUBERNETES CONFIGURATION
# =============================================================================

# OpenShift Cluster Configuration
# Set these environment variables or configure manually:
#   export OPENSHIFT_CLUSTER_URL="https://api.cluster.example.com:6443"
#   export OPENSHIFT_CLUSTER_TOKEN="your-token-here"
openshift_cluster_url = "https://api.cluster.example.com:6443"
openshift_cluster_token = "your-token-here"
openshift_cluster_ca_cert = ""
no_credentials_mode = {str(self.no_credentials).lower()}

'''

        return tfvars_content

    def convert(self, config, output_dir, verbose=False, full_yaml_data=None):
        """Convert YAML configuration to Terraform and write files to output directory."""
        self.verbose = verbose
        # Reset instance costs for this conversion
        self.instance_costs = []
        # Reset OpenShift cluster costs for this conversion
        self.openshift_costs = []
        
        # Set YAML data for GUID extraction - use full YAML data if provided
        yaml_data_for_guid = full_yaml_data if full_yaml_data is not None else config
        self.set_yaml_data(yaml_data_for_guid)
        
        # Start global section for setup operations
        self.start_global_section()
        
        # Validate cloud provider setup early
        self.validate_provider_setup(full_yaml_data or config)
        
        required_providers = self.detect_required_providers(config)

        # Generate the complete terraform configuration
        terraform_config = self.generate_complete_terraform(config, required_providers, full_yaml_data)
        
        # Write the main.tf file
        main_tf_path = os.path.join(output_dir, 'main.tf')
        with open(main_tf_path, 'w') as f:
            f.write(terraform_config)
        
        # Generate and write variables.tf
        variables_config = self.generate_variables_tf(required_providers, config)
        variables_path = os.path.join(output_dir, 'variables.tf')
        with open(variables_path, 'w') as f:
            f.write(variables_config)
        
        # Generate and write terraform.tfvars
        tfvars_config = self.generate_terraform_tfvars(required_providers, full_yaml_data or config)
        tfvars_path = os.path.join(output_dir, 'terraform.tfvars')
        with open(tfvars_path, 'w') as f:
            f.write(tfvars_config)
            
        # Generate ROSA CLI setup script if ROSA clusters are present AND using CLI deployment method
        if self.openshift_provider._has_rosa_clusters(config):
            # Check deployment method - only generate scripts for CLI method
            rosa_deployment = config.get('rosa_deployment', {})
            deployment_method = rosa_deployment.get('method', 'terraform')
            
            if deployment_method == 'cli':
                rosa_script = self.openshift_provider.generate_rosa_cli_script(config)
                script_path = os.path.join(output_dir, 'rosa-setup.sh')
                with open(script_path, 'w') as f:
                    f.write(rosa_script)
                # Make script executable
                os.chmod(script_path, 0o755)
                
                # Generate ROSA cleanup script
                cleanup_script = self.openshift_provider.generate_rosa_cleanup_script(config)
                if cleanup_script:
                    cleanup_path = os.path.join(output_dir, 'rosa-cleanup.sh')
                    with open(cleanup_path, 'w') as f:
                        f.write(cleanup_script)
                    # Make script executable
                    os.chmod(cleanup_path, 0o755)
                
                if self.verbose:
                    print()
                    print(f"Generated files:")
                    print(f"  - {main_tf_path}")
                    print(f"  - {variables_path}")
                    print(f"  - {tfvars_path}")
                    print(f"  - {script_path}")
                    if cleanup_script:
                        print(f"  - {cleanup_path}")
            else:
                # Terraform deployment method - no scripts generated
                if self.verbose:
                    print()
                    print(f"Generated files:")
                    print(f"  - {main_tf_path}")
                    print(f"  - {variables_path}")
                    print(f"  - {tfvars_path}")
        else:
            # No ROSA clusters
            if self.verbose:
                print()
                print(f"Generated files:")
                print(f"  - {main_tf_path}")
                print(f"  - {variables_path}")
                print(f"  - {tfvars_path}")



    def clean_name(self, name):
        """Clean a name for use as a Terraform resource identifier."""
        if not name:
            return "unnamed", False
        
        # Check if {guid} placeholder is present
        has_guid_placeholder = '{guid}' in name
        
        # Replace {guid} placeholder with actual GUID if present
        if has_guid_placeholder:
            guid = self.get_validated_guid()
            name = name.replace('{guid}', guid)
        
        cleaned_name = name.replace("-", "_").replace(".", "_").replace(" ", "_")
        return cleaned_name, has_guid_placeholder

    def replace_guid_placeholders(self, text):
        """Replace {guid} placeholders in text with actual GUID."""
        if not text or '{guid}' not in text:
            return text
        
        guid = self.get_validated_guid()
        return text.replace('{guid}', guid)

    def _resolve_instance_region_silent(self, instance, provider):
        """Resolve instance region silently for validation purposes (no output)."""
        # CNV provider doesn't use regions - return None
        if provider == 'cnv':
            return None
            
        # Create cache key to prevent multiple validations for the same instance
        instance_name = instance.get('name', 'unnamed')
        cache_key = f"{instance_name}_{provider}_{instance.get('region', '')}_{instance.get('location', '')}"
        
        # Return cached result if available
        if cache_key in self._region_cache:
            return self._region_cache[cache_key]
        
        has_region = 'region' in instance
        has_location = 'location' in instance
        find_best_region_on_fail = instance.get('find_best_region_on_fail', False)

        if has_region and has_location:
            raise ValueError(f"Instance '{instance_name}' cannot specify both 'region' and 'location'.")

        if not has_region and not has_location:
            raise ValueError(f"Instance '{instance_name}' must specify either 'region' or 'location'.")

        resolved_region = None

        if has_location:
            # Location-based: Auto-select closest region with validation
            location_key = instance['location']
            mapped_region = None
            
            # First try to get mapped region from locations - ERROR if not found
            if location_key in self.locations:
                mapped_region = self.locations[location_key].get(provider)
                if not mapped_region:
                    raise ValueError(f"Instance '{instance_name}': Location '{location_key}' is not supported for provider '{provider}'. "
                                   f"Check mappings/locations.yaml for supported locations.")
            else:
                raise ValueError(f"Instance '{instance_name}': Location '{location_key}' not found in location mappings. "
                               f"Check mappings/locations.yaml for supported locations.")
            
            # For location-based, validate and auto-select best region if needed
            instance_type = self._get_instance_type_for_validation(instance, provider)
            if instance_type and provider == 'gcp':
                # Check if the mapped region supports the instance type
                if not self.gcp_provider.check_machine_type_availability(instance_type, mapped_region, silent=True):
                    # Auto-select best available region
                    available_regions = self.gcp_provider.find_available_regions_for_machine_type(instance_type)
                    best_region = self.gcp_provider.find_closest_available_region(mapped_region, available_regions)
                    
                    if best_region:
                        resolved_region = best_region
                    else:
                        raise ValueError(f"Instance '{instance_name}': No available regions found for machine type '{instance_type}' near location '{location_key}'.")
                else:
                    resolved_region = mapped_region
            else:
                resolved_region = mapped_region

        if has_region:
            # Region-based: Validate and ERROR OUT if invalid (don't continue)
            requested_region = instance['region']
            
            # Get the instance type to validate
            instance_type = self._get_instance_type_for_validation(instance, provider)
            
            if instance_type and provider == 'gcp':
                # Check if the machine type is available in the requested region
                if not self.gcp_provider.check_machine_type_availability(instance_type, requested_region, silent=True):
                    available_regions = self.gcp_provider.find_available_regions_for_machine_type(instance_type)
                    
                    if find_best_region_on_fail:
                        # Auto-select closest available region
                        best_region = self.gcp_provider.find_closest_available_region(requested_region, available_regions)
                        
                        if best_region:
                            resolved_region = best_region
                        else:
                            raise ValueError(f"Instance '{instance_name}': Machine type '{instance_type}' not available in any region.")
                    else:
                        # Error out and stop execution
                        if available_regions:
                            suggestion = f"Try: {', '.join(available_regions[:3])}"
                            raise ValueError(f"Instance '{instance_name}': Machine type '{instance_type}' not available in region '{requested_region}'. "
                                           f"Available regions: {', '.join(available_regions)}. "
                                           f"Suggestion: {suggestion} or set find_best_region_on_fail: true")
                        else:
                            raise ValueError(f"Instance '{instance_name}': Machine type '{instance_type}' not available in any region.")
                else:
                    resolved_region = requested_region
            else:
                resolved_region = requested_region
        
        # Cache the result
        self._region_cache[cache_key] = resolved_region
        return resolved_region

    def resolve_instance_region(self, instance, provider):
        """Resolve instance region with support for both direct regions and mapped locations."""
        # CNV provider doesn't use regions - return None
        if provider == 'cnv':
            return None
            
        # Create cache key to prevent multiple validations for the same instance
        instance_name = instance.get('name', 'unnamed')
        cache_key = f"{instance_name}_{provider}_{instance.get('region', '')}_{instance.get('location', '')}"
        
        # Return cached result if available
        if cache_key in self._region_cache:
            return self._region_cache[cache_key]
        
        has_region = 'region' in instance
        has_location = 'location' in instance
        find_best_region_on_fail = instance.get('find_best_region_on_fail', False)

        if has_region and has_location:
            raise ValueError(f"Instance '{instance_name}' cannot specify both 'region' and 'location'.")

        if not has_region and not has_location:
            raise ValueError(f"Instance '{instance_name}' must specify either 'region' or 'location'.")

        resolved_region = None

        if has_location:
            # Location-based: Auto-select closest region with validation
            location_key = instance['location']
            mapped_region = None
            
            # First try to get mapped region from locations - ERROR if not found
            if location_key in self.locations:
                mapped_region = self.locations[location_key].get(provider)
                if not mapped_region:
                    raise ValueError(f"Instance '{instance_name}': Location '{location_key}' is not supported for provider '{provider}'. "
                                   f"Check mappings/locations.yaml for supported locations.")
            else:
                raise ValueError(f"Instance '{instance_name}': Location '{location_key}' not found in location mappings. "
                               f"Check mappings/locations.yaml for supported locations.")
            
            # Location mapping will be shown in the Region line below
            
            # For location-based, validate and auto-select best region if needed
            instance_type = self._get_instance_type_for_validation(instance, provider)
            if instance_type and provider == 'gcp':
                if hasattr(self, 'verbose') and self.verbose:
                    self.print_instance_output(instance_name, provider, f"Checking GCP region availability for machine type '{instance_type}' in region '{mapped_region}'...")
                # Check if the mapped region supports the instance type
                if not self.gcp_provider.check_machine_type_availability(instance_type, mapped_region):
                    # Auto-select best available region
                    if hasattr(self, 'verbose') and self.verbose:
                        self.print_instance_output(instance_name, provider, f"Finding alternative GCP regions for machine type '{instance_type}'...")
                    available_regions = self.gcp_provider.find_available_regions_for_machine_type(instance_type)
                    best_region = self.gcp_provider.find_closest_available_region(mapped_region, available_regions)
                    
                    if best_region:
                        self.print_instance_output(instance_name, provider, f"WARNING: Location '{location_key}' maps to region '{mapped_region}' which doesn't support machine type '{instance_type}'. Auto-selecting closest available region: '{best_region}'")
                        resolved_region = best_region
                    else:
                        raise ValueError(f"Instance '{instance_name}': No available regions found for machine type '{instance_type}' near location '{location_key}'.")
                else:
                    resolved_region = mapped_region
            else:
                resolved_region = mapped_region

        if has_region:
            # Region-based: Validate and ERROR OUT if invalid (don't continue)
            requested_region = instance['region']
            
            # Get the instance type to validate
            instance_type = self._get_instance_type_for_validation(instance, provider)
            
            if instance_type and provider == 'gcp':
                if hasattr(self, 'verbose') and self.verbose:
                    self.print_instance_output(instance_name, provider, f"Checking GCP region availability for machine type '{instance_type}' in region '{requested_region}'...")
                # Check if the machine type is available in the requested region
                if not self.gcp_provider.check_machine_type_availability(instance_type, requested_region):
                    self.print_instance_output(instance_name, provider, f"Finding alternative GCP regions for machine type '{instance_type}'...")
                    available_regions = self.gcp_provider.find_available_regions_for_machine_type(instance_type)
                    
                    if find_best_region_on_fail:
                        # Auto-select closest available region
                        best_region = self.gcp_provider.find_closest_available_region(requested_region, available_regions)
                        
                        if best_region:
                            self.print_instance_output(instance_name, provider, f"WARNING: Machine type '{instance_type}' not available in region '{requested_region}'. Auto-selecting closest available region: '{best_region}'")
                            resolved_region = best_region
                        else:
                            raise ValueError(f"Instance '{instance_name}': Machine type '{instance_type}' not available in any region.")
                    else:
                        # Error out and stop execution
                        if available_regions:
                            suggestion = f"Try: {', '.join(available_regions[:3])}"
                            raise ValueError(f"Instance '{instance_name}': Machine type '{instance_type}' not available in region '{requested_region}'. "
                                           f"Available regions: {', '.join(available_regions)}. "
                                           f"Suggestion: {suggestion} or set find_best_region_on_fail: true")
                        else:
                            raise ValueError(f"Instance '{instance_name}': Machine type '{instance_type}' not available in any region.")
                else:
                    resolved_region = requested_region
            else:
                resolved_region = requested_region
        
        # Cache the result
        self._region_cache[cache_key] = resolved_region
        return resolved_region

    def _get_instance_type_for_validation(self, instance, provider):
        """Get the instance type for validation purposes."""
        # Get the flavor from the instance
        flavor = instance.get('flavor')
        gpu_type = instance.get('gpu_type')
        
        if not flavor:
            return None
        
        # For GCP, resolve the machine type considering GPU requirements
        if provider == 'gcp':
            try:
                # If this is a GPU instance, we need to find the actual machine type with GPU
                if gpu_type:
                    # Find the cheapest GPU instance type for this provider to get the actual machine type
                    provider_costs = self.find_cheapest_gpu_by_specs(gpu_type)
                    if provider in provider_costs:
                        return provider_costs[provider]['instance_type']
                    
                    # Fallback: look through flavor mappings for GPU instances
                    provider_flavors = self.flavors.get(provider, {})
                    flavor_mappings = provider_flavors.get('flavor_mappings', {})
                    
                    # Look for flavor/GPU combination in flavor mappings
                    for flavor_category, flavor_options in flavor_mappings.items():
                        if flavor_category == flavor or flavor in flavor_category:
                            for instance_type, specs in flavor_options.items():
                                if (specs.get('gpu_count', 0) > 0 and 
                                    self.gpu_type_matches(specs.get('gpu_type', ''), gpu_type)):
                                    return instance_type
                    
                    # Also check machine types
                    machine_types = provider_flavors.get('machine_types', {})
                    for instance_type, specs in machine_types.items():
                        if (specs.get('gpu_count', 0) > 0 and 
                            self.gpu_type_matches(specs.get('gpu_type', ''), gpu_type)):
                            return instance_type
                
                # For non-GPU instances, use the regular machine type resolution
                return self.gcp_provider.get_gcp_machine_type(flavor)
                
            except ValueError:
                # If we can't resolve the machine type, assume it's already a machine type
                return flavor
        
        # For other providers, return the flavor as-is
        return flavor

    def get_all_aws_regions(self, yaml_data):
        """Get all AWS regions used in the deployment configuration."""
        aws_regions = set()
        
        # Collect regions from AWS instances
        for instance in yaml_data.get('instances', []):
            provider = instance.get('provider')
            
            # Resolve meta providers to actual providers
            if provider == 'cheapest':
                provider = self.find_cheapest_provider(instance, suppress_output=True)
            elif provider == 'cheapest-gpu':
                provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
            
            if provider == 'aws':
                try:
                    region = self._resolve_instance_region_silent(instance, 'aws')
                    aws_regions.add(region)
                except ValueError:
                    # Instance doesn't specify region - this should be a validation error
                    # Don't add any fallback region
                    pass
        
        # Collect regions from OpenShift clusters
        for cluster in yaml_data.get('openshift_clusters', []):
            cluster_type = cluster.get('type', '')
            if cluster_type in ['rosa-classic', 'rosa-hcp']:
                region = cluster.get('region')
                if region:
                    aws_regions.add(region)
        
        # Return sorted list of regions (primary first)
        regions_list = sorted(aws_regions) if aws_regions else []
        return regions_list
    
    def get_primary_aws_region(self, yaml_data):
        """Get the primary AWS region (first in sorted order)."""
        regions = self.get_all_aws_regions(yaml_data)
        if not regions:
            return None  # No regions found - let caller handle this
        return regions[0]
    
    def get_aws_provider_reference(self, region, all_regions):
        """Get the Terraform provider reference for a specific AWS region."""
        if not all_regions:
            return ""
        
        primary_region = all_regions[0]
        
        # Primary region uses default provider
        if region == primary_region:
            return ""
        
        # Other regions use aliased providers
        clean_region = region.replace("-", "_").replace(".", "_")
        return f"provider = aws.{clean_region}"

    def find_closest_flavor_for_provider(self, provider, cores, memory_mb, gpus=None, gpu_type=None):
        """Find the closest matching flavor for a specific provider given hardware requirements."""
        memory_gb = memory_mb / 1024
        
        # Get provider-specific flavor mappings
        provider_flavors = self.flavors.get(provider, {})
        flavor_mappings = provider_flavors.get('flavor_mappings', {})
        
        best_match = None
        best_score = float('inf')
        
        # Search through all flavor categories for this provider
        for flavor_name, flavor_options in flavor_mappings.items():
            for instance_type, specs in flavor_options.items():
                instance_cores = specs.get('vcpus', 0)
                instance_memory_gb = specs.get('memory_gb', 0)
                instance_gpus = specs.get('gpu_count', 0)
                instance_gpu_type = specs.get('gpu_type')
                cost = specs.get('hourly_cost')
                
                # Check if this instance meets minimum requirements
                meets_cpu = instance_cores >= cores
                meets_memory = instance_memory_gb >= memory_gb
                meets_gpu_count = gpus is None or instance_gpus >= gpus
                meets_gpu_type = gpu_type is None or (instance_gpu_type and self.gpu_type_matches(instance_gpu_type, gpu_type))
                
                if meets_cpu and meets_memory and meets_gpu_count and meets_gpu_type:
                    # Calculate score based on resource overhead (lower is better)
                    cpu_overhead = instance_cores - cores
                    memory_overhead = instance_memory_gb - memory_gb
                    gpu_overhead = instance_gpus - (gpus or 0)
                    
                    # Weight the score to favor CPU accuracy over memory, and minimize cost
                    score = (cpu_overhead * 2) + memory_overhead + (gpu_overhead * 3)
                    if cost is not None:
                        score += cost * 0.1  # Small cost factor for tie-breaking
                    
                    if score < best_score:
                        best_score = score
                        best_match = {
                            'instance_type': instance_type,
                            'flavor': flavor_name,
                            'cost': cost,
                            'vcpus': instance_cores,
                            'memory_gb': instance_memory_gb,
                            'gpu_count': instance_gpus,
                            'gpu_type': instance_gpu_type
                        }
        
        return best_match

    def find_closest_flavor(self, cores, memory_mb, gpus=None, gpu_type=None):
        """Find the closest matching generic flavor for given hardware requirements."""
        memory_gb = memory_mb / 1024
        
        best_matches = []
        
        # Check all generic flavors
        for flavor_name, flavor_config in self.flavors.items():
            # Skip provider-specific flavors (they're nested dictionaries)
            if isinstance(flavor_config, dict) and 'description' in flavor_config:
                # This is a generic flavor
                
                # Check if it has the required providers (we need at least one provider mapping)
                available_providers = [p for p in ['aws', 'azure', 'gcp', 'ibm_vpc', 'ibm_classic'] if p in flavor_config]
                if not available_providers:
                    continue
                
                # Analyze this flavor across providers to get average specs
                total_vcpus = 0
                total_memory = 0
                total_gpus = 0
                gpu_types_found = set()
                valid_providers = 0
                
                for provider in available_providers:
                    instance_type = flavor_config[provider]
                    provider_flavors = self.flavors.get(provider, {})
                    
                    # Try to get specs from provider flavor mappings
                    specs = self.get_instance_specs_from_provider(provider, instance_type, flavor_name, provider_flavors)
                    if specs:
                        total_vcpus += specs.get('vcpus', 0)
                        total_memory += specs.get('memory_gb', 0) 
                        total_gpus += specs.get('gpu_count', 0)
                        if specs.get('gpu_type'):
                            gpu_types_found.add(specs['gpu_type'])
                        valid_providers += 1
                
                if valid_providers == 0:
                    continue
                
                # Calculate average specs
                avg_vcpus = total_vcpus / valid_providers
                avg_memory = total_memory / valid_providers
                avg_gpus = total_gpus / valid_providers
                
                # Check if this flavor meets requirements
                meets_cpu = avg_vcpus >= cores
                meets_memory = avg_memory >= memory_gb
                meets_gpu_count = gpus is None or avg_gpus >= gpus
                meets_gpu_type = gpu_type is None or any(self.gpu_type_matches(found_type, gpu_type) for found_type in gpu_types_found)
                
                if meets_cpu and meets_memory and meets_gpu_count and meets_gpu_type:
                    # Calculate efficiency score (lower is better - means less over-provisioning)
                    cpu_overhead = avg_vcpus - cores
                    memory_overhead = avg_memory - memory_gb
                    gpu_overhead = avg_gpus - (gpus or 0)
                    
                    # Weighted efficiency score
                    efficiency_score = (cpu_overhead * 1.0) + (memory_overhead * 0.5) + (gpu_overhead * 2.0)
                    
                    # If no GPU was requested, heavily penalize GPU flavors to prefer non-GPU options
                    if gpus is None and avg_gpus > 0:
                        efficiency_score += 1000  # Heavy penalty for unwanted GPU
                    
                    # Prefer generic flavors over GPU-specific flavors when appropriate
                    is_gpu_flavor = any(gpu_prefix in flavor_name for gpu_prefix in ['gpu_', 'gpu_t4_', 'gpu_v100_', 'gpu_a100_', 'gpu_amd_'])
                    if gpus is None and is_gpu_flavor:
                        efficiency_score += 500  # Medium penalty for GPU-specific flavors when no GPU requested
                    
                    best_matches.append({
                        'flavor': flavor_name,
                        'avg_vcpus': avg_vcpus,
                        'avg_memory_gb': avg_memory,
                        'avg_gpus': avg_gpus,
                        'gpu_types': list(gpu_types_found),
                        'efficiency_score': efficiency_score,
                        'cpu_overhead': cpu_overhead,
                        'memory_overhead': memory_overhead,
                        'gpu_overhead': gpu_overhead,
                        'available_providers': available_providers,
                        'is_gpu_flavor': is_gpu_flavor
                    })
        
        if not best_matches:
            return None
        
        # Sort by efficiency score (best match first)
        best_matches.sort(key=lambda x: x['efficiency_score'])
        return best_matches[0]

    def get_instance_specs_from_provider(self, provider, instance_type, flavor_name, provider_flavors):
        """Get instance specifications from provider flavor mappings."""
        # Check flavor mappings first
        flavor_mappings = provider_flavors.get('flavor_mappings', {})
        for size_category, size_options in flavor_mappings.items():
            if instance_type in size_options:
                return size_options[instance_type]
        
        # Check direct machine type mappings (for GCP)
        machine_types = provider_flavors.get('machine_types', {})
        if instance_type in machine_types:
            return machine_types[instance_type]
        
        return None

    def find_cheapest_provider(self, instance, suppress_output=False):
        """Find the cheapest cloud provider for given instance requirements."""
        # Apply flavor-to-cores/memory conversion for cheapest provider
        instance = self._convert_flavor_to_specs_for_cheapest(instance)
        
        size = instance.get("flavor")  # Changed from "size" to "flavor"
        cores = instance.get("cores")
        memory = instance.get("memory")  # in MB
        gpu_count = instance.get("gpu_count")  # Number of GPUs required
        gpu_type = instance.get("gpu_type")  # Specific GPU type requirement
        find_flavor = instance.get("find_flavor", False)  # NEW: Auto-find closest flavor
        
        # Validate GPU type if specified
        if gpu_type:
            self.validate_gpu_type(gpu_type)
        
        # NEW: Auto-find closest flavor if requested
        if find_flavor and cores and memory:
            closest_flavor = self.find_closest_flavor(cores, memory, gpu_count, gpu_type)
            if closest_flavor:
                memory_gb = memory / 1024
                req_desc = f"{cores} cores, {memory_gb:.1f}GB RAM"
                if gpu_count and gpu_type:
                    req_desc += f", {gpu_count} {gpu_type} GPU(s)"
                elif gpu_count:
                    req_desc += f", {gpu_count} GPU(s)"
                
                print(f"Auto-matched flavor for {req_desc}:")
                print(f"  Recommended flavor: {closest_flavor['flavor']}")
            print(f"  Avg specs: {closest_flavor['avg_vcpus']:.1f} vCPUs, {closest_flavor['avg_memory_gb']:.1f}GB RAM", end="")
            if closest_flavor['avg_gpus'] > 0:
                gpu_info = f", {closest_flavor['avg_gpus']:.1f} GPUs"
                if closest_flavor['gpu_types']:
                    gpu_info += f" ({', '.join(closest_flavor['gpu_types'])})"
                print(gpu_info)
            else:
                print()
            print(f"  Available on: {', '.join(closest_flavor['available_providers'])}")
            
            # Use the found flavor instead of specs
            size = closest_flavor['flavor']
            # Clear the hardware specs since we're now using flavor
            cores = None
            memory = None
        
        # Get cost information for all providers
        provider_costs = {}
        
        # Get instance-specific exclusions
        instance_exclusions = instance.get('exclude_providers', [])
        
        # Prioritize specs over size for cheapest provider selection
        if cores and memory:
            # Hardware specification-based selection (with optional GPU) - preferred for cheapest
            provider_costs = self.find_cheapest_by_specs(cores, memory, gpu_count, gpu_type, instance_exclusions)
            memory_gb = memory / 1024
            
            if gpu_count and gpu_type:
                analysis_type = f"{cores} cores, {memory_gb:.1f}GB RAM, {gpu_count} {gpu_type} GPU(s)"
            elif gpu_count:
                analysis_type = f"{cores} cores, {memory_gb:.1f}GB RAM, {gpu_count} GPU(s)"
            else:
                analysis_type = f"{cores} cores, {memory_gb:.1f}GB RAM"
        elif size:
            # Flavor-based selection (fallback when no specs provided)
            provider_costs = self.find_cheapest_by_size(size, gpu_type, instance_exclusions)
            analysis_type = f"flavor '{size}'"
            if gpu_type:
                analysis_type += f" with {gpu_type} GPU"
        else:
            # No fallback - must specify requirements
            instance_name = instance.get('name', 'unnamed')
            raise ValueError(
                f"Instance '{instance_name}': When using provider 'cheapest', you must specify either:\n"
                f"  - 'flavor': nano, micro, small, medium, large, xlarge, etc.\n"
                f"  - 'cores' and 'memory': e.g., cores: 2, memory: 4096 (MB)\n"
                f"  - GPU requirements: gpu_count, gpu_type, etc."
            )
        
        if not provider_costs:
            # Fallback to AWS if no cost information available
            if not suppress_output:
                print(f"Warning: No cost information found for {analysis_type}, defaulting to AWS")
            return 'aws'
        
        # Apply discounts to all provider costs before finding cheapest
        for provider in provider_costs:
            original_cost = provider_costs[provider]['cost']
            provider_costs[provider]['original_cost'] = original_cost
            provider_costs[provider]['cost'] = self.apply_discount(original_cost, provider)
        
        # Find the cheapest provider
        cheapest_provider = min(provider_costs.keys(), key=lambda p: provider_costs[p]['cost'])
        
        # Only print cost analysis if not suppressed
        if not suppress_output:
            instance_name = instance.get('name', 'unnamed')
            # Get instance-specific exclusions
            instance_exclusions = instance.get('exclude_providers', [])
            # Log provider exclusions if any (always suppress since they're shown under instance name)
            self.log_provider_exclusions("cheapest provider selection", suppress_output, instance_exclusions, True)
            # Resolve GUID in instance name before showing cost analysis
            resolved_instance_name = self.replace_guid_placeholders(instance_name)
            print(f"   Cost analysis for instance '{resolved_instance_name}':")
            for provider, info in sorted(provider_costs.items(), key=lambda x: x[1]['cost']):
                marker = " ← SELECTED" if provider == cheapest_provider else ""
                vcpus = info.get('vcpus', 'N/A')
                memory_gb = info.get('memory_gb', 'N/A')
                gpu_count = info.get('gpu_count', 0)
                detected_gpu_type = info.get('gpu_type', '')
                
                # Format cost display with discount info
                cost_display = self._format_cost_with_discount(provider, info.get('original_cost'), info['cost'])
                
                if gpu_count > 0:
                    gpu_info = f", {gpu_count}x {detected_gpu_type}" if detected_gpu_type else f", {gpu_count} GPU(s)"
                    print(f"     {provider}: {cost_display} ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB{gpu_info}){marker}")
                else:
                    print(f"     {provider}: {cost_display} ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB){marker}")
        
        return cheapest_provider
    
    def find_cheapest_gpu_provider(self, instance, suppress_output=False):
        """Find the cheapest cloud provider for GPU instances, ignoring cores/memory requirements."""
        gpu_type = instance.get("gpu_type")  # Specific GPU type requirement
        
        # Validate GPU type if specified
        if gpu_type:
            self.validate_gpu_type(gpu_type)
        
        # Get instance-specific exclusions
        instance_exclusions = instance.get('exclude_providers', [])
        
        # Get cost information for all providers (GPU instances only)
        provider_costs = self.find_cheapest_gpu_by_specs(gpu_type, instance_exclusions)
        
        if gpu_type:
            analysis_type = f"cheapest {gpu_type} GPU"
        else:
            analysis_type = "cheapest GPU (any type)"
        
        if not provider_costs:
            # Fallback to AWS if no cost information available
            if not suppress_output:
                print(f"Warning: No GPU cost information found for {analysis_type}, defaulting to AWS")
            return 'aws'
        
        # Apply discounts to all provider costs before finding cheapest
        for provider in provider_costs:
            original_cost = provider_costs[provider]['cost']
            provider_costs[provider]['original_cost'] = original_cost
            provider_costs[provider]['cost'] = self.apply_discount(original_cost, provider)
        
        # Find the cheapest provider
        cheapest_provider = min(provider_costs.keys(), key=lambda p: provider_costs[p]['cost'])
        
        # Only print cost analysis if not suppressed
        if not suppress_output:
            instance_name = instance.get('name', 'unnamed')
            # Log provider exclusions if any (always suppress since they're shown under instance name)
            self.log_provider_exclusions("cheapest GPU provider selection", suppress_output, instance_exclusions, True)
            print(f"   GPU-optimized cost analysis for instance '{instance_name}':")
            for provider, info in sorted(provider_costs.items(), key=lambda x: x[1]['cost']):
                marker = " ← SELECTED" if provider == cheapest_provider else ""
                vcpus = info.get('vcpus', 'N/A')
                memory_gb = info.get('memory_gb', 'N/A')
                gpu_count = info.get('gpu_count', 0)
                detected_gpu_type = info.get('gpu_type', '')
                
                # Format cost display with discount info
                cost_display = self._format_cost_with_discount(provider, info.get('original_cost'), info['cost'])
                
                gpu_info = f", {gpu_count}x {detected_gpu_type}" if detected_gpu_type else f", {gpu_count} GPU(s)"
                print(f"     {provider}: {cost_display} ({info['instance_type']}, {vcpus} vCPU, {memory_gb}GB{gpu_info}){marker}")
        
        return cheapest_provider
    
    def find_cheapest_gpu_by_specs(self, gpu_type=None, instance_exclusions=None):
        """Find cheapest GPU instances across all providers, ignoring CPU/memory constraints."""
        provider_costs = {}
        
        # Get available providers (excluding those configured to be excluded from cheapest)
        available_providers = self.get_effective_providers(instance_exclusions=instance_exclusions)
        
        # Check each provider's flavor mappings for GPU instances
        for provider in available_providers:
            provider_flavors = self.flavors.get(provider, {})
            best_option = None
            
            # Check flavor mappings for GPU options
            flavor_mappings = provider_flavors.get('flavor_mappings', {})
            for size_category, size_options in flavor_mappings.items():
                for instance_type, specs in size_options.items():
                    vcpus = specs.get('vcpus', 0)
                    memory_gb = specs.get('memory_gb', 0)
                    gpus = specs.get('gpu_count', 0)
                    cost = specs.get('hourly_cost')
                    instance_gpu_type = specs.get('gpu_type', '')
                    
                    # Only consider instances with GPUs
                    if gpus <= 0 or cost is None:
                        continue
                    
                    # If specific GPU type is required, check GPU type match
                    if gpu_type and not self.gpu_type_matches(instance_gpu_type, gpu_type):
                        continue
                    
                    # If this is cheaper than current best option, use it
                    if best_option is None or cost < best_option['cost']:
                        best_option = {
                            'cost': cost,
                            'instance_type': instance_type,
                            'vcpus': vcpus,
                            'memory_gb': memory_gb,
                            'gpu_count': gpus,
                            'gpu_type': instance_gpu_type,
                            'gpu_memory_gb': specs.get('gpu_memory_gb')
                        }
            
            # Also check direct machine type mappings (for GCP)
            machine_types = provider_flavors.get('machine_types', {})
            for instance_type, specs in machine_types.items():
                vcpus = specs.get('vcpus', 0)
                memory_gb = specs.get('memory_gb', 0)
                gpus = specs.get('gpu_count', 0)
                cost = specs.get('hourly_cost')
                instance_gpu_type = specs.get('gpu_type', '')
                
                # Only consider instances with GPUs
                if gpus <= 0 or cost is None:
                    continue
                
                # If specific GPU type is required, check GPU type match
                if gpu_type and not self.gpu_type_matches(instance_gpu_type, gpu_type):
                    continue
                
                # If this is cheaper than current best option, use it
                if best_option is None or cost < best_option['cost']:
                    best_option = {
                        'cost': cost,
                        'instance_type': instance_type,
                        'vcpus': vcpus,
                        'memory_gb': memory_gb,
                        'gpu_count': gpus,
                        'gpu_type': instance_gpu_type,
                        'gpu_memory_gb': specs.get('gpu_memory_gb')
                    }
            
            if best_option:
                provider_costs[provider] = best_option
        
        return provider_costs

    def get_cheapest_gpu_instance_type(self, instance, provider):
        """Get the cheapest GPU instance type for the selected provider."""
        gpu_type = instance.get("gpu_type")
        
        # Find the cheapest GPU option for this provider
        provider_costs = self.find_cheapest_gpu_by_specs(gpu_type)
        
        if provider in provider_costs:
            instance_type = provider_costs[provider]['instance_type']
            if instance_type:  # Make sure it's not None
                return instance_type
        
        # Fallback to default GPU instance type
        provider_flavors = self.flavors.get(provider, {})
        
        # Try to find a default GPU instance
        flavor_mappings = provider_flavors.get('flavor_mappings', {})
        for size_category, size_options in flavor_mappings.items():
            for instance_type, specs in size_options.items():
                if specs.get('gpu_count', 0) > 0:
                    return instance_type
        
        # Final fallback
        return "gpu_small"  # This should be mapped in flavor files
    
    def find_cheapest_by_size(self, size, gpu_type=None, instance_exclusions=None):
        """Find cheapest provider for a generic size, optionally filtered by GPU type."""
        provider_costs = {}
        
        # Get available providers (excluding those configured to be excluded from cheapest)
        available_providers = self.get_effective_providers(instance_exclusions=instance_exclusions)
        
        # Check generic flavors for cost comparison
        if size in self.flavors:
            generic_flavor = self.flavors[size]
            
            # Calculate costs for each provider
            for provider in available_providers:
                if provider in generic_flavor:
                    instance_type = generic_flavor[provider]
                    
                    # Look up cost in provider-specific flavor mappings
                    provider_flavors = self.flavors.get(provider, {})
                    cost_info = self.get_instance_cost_info(provider, instance_type, size, provider_flavors)
                    
                    if cost_info:
                        # Filter by GPU type if specified
                        if gpu_type:
                            instance_gpu_type = cost_info.get('gpu_type', '')
                            # Normalize GPU type comparison (case-insensitive, partial match)
                            if not self.gpu_type_matches(instance_gpu_type, gpu_type):
                                continue
                        
                        provider_costs[provider] = cost_info
        
        return provider_costs
    
    def _convert_flavor_to_specs_for_cheapest(self, instance):
        """Convert generic flavor names to cores/memory for cheapest provider analysis."""
        instance = instance.copy()
        flavor = instance.get('flavor')
        
        # Only convert if we have a flavor but no cores/memory specs
        if flavor and not (instance.get('cores') and instance.get('memory')):
            # Load cheapest provider flavor mappings
            cheapest_flavors = self.flavors.get('cheapest', {})
            flavor_mappings = cheapest_flavors.get('flavor_mappings', {})
            
            if flavor in flavor_mappings:
                flavor_options = flavor_mappings[flavor]
                # Get the first (and typically only) option for this flavor
                flavor_key = next(iter(flavor_options.keys()))
                flavor_config = flavor_options[flavor_key]
                
                if 'cores' in flavor_config and 'memory' in flavor_config:
                    instance['cores'] = flavor_config['cores']
                    instance['memory'] = flavor_config['memory']
                    # Keep the flavor for reference, but cores/memory take precedence
        
        return instance
    
    def find_cheapest_by_specs(self, required_cores, required_memory_mb, required_gpus=None, gpu_type=None, instance_exclusions=None):
        """Find cheapest provider for specific CPU/memory/GPU requirements."""
        provider_costs = {}
        required_memory_gb = required_memory_mb / 1024
        
        # Get available providers (excluding those configured to be excluded from cheapest)
        available_providers = self.get_effective_providers(instance_exclusions=instance_exclusions)
        
        # Check each provider's flavor mappings for instances that meet requirements
        for provider in available_providers:
            provider_flavors = self.flavors.get(provider, {})
            best_option = None
            
            # Check flavor mappings for options that meet requirements
            flavor_mappings = provider_flavors.get('flavor_mappings', {})
            for size_category, size_options in flavor_mappings.items():
                for instance_type, specs in size_options.items():
                    vcpus = specs.get('vcpus', 0)
                    memory_gb = specs.get('memory_gb', 0)
                    gpus = specs.get('gpu_count', 0)
                    cost = specs.get('hourly_cost')
                    instance_gpu_type = specs.get('gpu_type', '')
                    
                    # Check if this instance meets requirements
                    meets_requirements = (
                        vcpus >= required_cores and 
                        memory_gb >= required_memory_gb and 
                        cost is not None
                    )
                    
                    # If GPU is required, check GPU requirements
                    if required_gpus is not None:
                        meets_requirements = meets_requirements and gpus >= required_gpus
                    
                    # If specific GPU type is required, check GPU type match
                    if gpu_type and gpus > 0:
                        meets_requirements = meets_requirements and self.gpu_type_matches(instance_gpu_type, gpu_type)
                    
                    if meets_requirements:
                        # If this is cheaper than current best option, use it
                        if best_option is None or cost < best_option['cost']:
                            best_option = {
                                'cost': cost,
                                'instance_type': instance_type,
                                'vcpus': vcpus,
                                'memory_gb': memory_gb,
                                'gpus': gpus,
                                'gpu_type': instance_gpu_type,
                                'gpu_memory_gb': specs.get('gpu_memory_gb')
                            }
            
            # Also check direct machine type mappings (for GCP)
            machine_types = provider_flavors.get('machine_types', {})
            for instance_type, specs in machine_types.items():
                vcpus = specs.get('vcpus', 0)
                memory_gb = specs.get('memory_gb', 0)
                gpus = specs.get('gpu_count', 0)
                cost = specs.get('hourly_cost')
                instance_gpu_type = specs.get('gpu_type', '')
                
                # Check if this instance meets requirements
                meets_requirements = (
                    vcpus >= required_cores and 
                    memory_gb >= required_memory_gb and 
                    cost is not None
                )
                
                # If GPU is required, check GPU requirements
                if required_gpus is not None:
                    meets_requirements = meets_requirements and gpus >= required_gpus
                
                # If specific GPU type is required, check GPU type match
                if gpu_type and gpus > 0:
                    meets_requirements = meets_requirements and self.gpu_type_matches(instance_gpu_type, gpu_type)
                
                if meets_requirements:
                    # If this is cheaper than current best option, use it
                    if best_option is None or cost < best_option['cost']:
                        best_option = {
                            'cost': cost,
                            'instance_type': instance_type,
                            'vcpus': vcpus,
                            'memory_gb': memory_gb,
                            'gpus': gpus,
                            'gpu_type': instance_gpu_type,
                            'gpu_memory_gb': specs.get('gpu_memory_gb')
                        }
            
            if best_option:
                provider_costs[provider] = best_option
        
        return provider_costs
    
    def gpu_type_matches(self, instance_gpu_type, required_gpu_type):
        """Check if instance GPU type matches the required GPU type."""
        if not instance_gpu_type or not required_gpu_type:
            return False
        
        # Normalize for comparison (case-insensitive)
        instance_type = instance_gpu_type.upper().strip()
        required_type = required_gpu_type.upper().strip()
        
        # Handle common GPU type patterns
        gpu_patterns = {
            'NVIDIA T4': ['T4', 'NVIDIA T4'],
            'NVIDIA V100': ['V100', 'NVIDIA V100'],
            'NVIDIA A100': ['A100', 'NVIDIA A100'],
            'NVIDIA L4': ['L4', 'NVIDIA L4'],
            'NVIDIA L40S': ['L40S', 'NVIDIA L40S'],
            'AMD RADEON PRO V520': ['AMD', 'RADEON', 'V520', 'AMD RADEON PRO V520'],
            'NVIDIA K80': ['K80', 'NVIDIA K80']
        }
        
        # Check for direct match
        if required_type in instance_type or instance_type in required_type:
            return True
        
        # Check pattern matches
        for full_name, patterns in gpu_patterns.items():
            if required_type in patterns or required_type == full_name:
                return any(pattern in instance_type for pattern in patterns)
        
        return False
    
    def validate_gpu_type(self, requested_gpu_type):
        """Validate that the requested GPU type exists in our flavor mappings."""
        available_gpu_types = set()
        
        # Collect all GPU types from all providers (don't exclude any for validation)
        all_providers = ['aws', 'azure', 'gcp', 'ibm_vpc', 'ibm_classic', 'oci', 'vmware', 'alibaba']
        for provider in all_providers:
            provider_flavors = self.flavors.get(provider, {})
            
            # Check flavor mappings
            flavor_mappings = provider_flavors.get('flavor_mappings', {})
            for size_category, size_options in flavor_mappings.items():
                for instance_type, specs in size_options.items():
                    gpu_type = specs.get('gpu_type')
                    if gpu_type:
                        available_gpu_types.add(gpu_type)
            
            # Check direct machine type mappings (for GCP)
            machine_types = provider_flavors.get('machine_types', {})
            for instance_type, specs in machine_types.items():
                gpu_type = specs.get('gpu_type')
                if gpu_type:
                    available_gpu_types.add(gpu_type)
        

        
        for available_gpu in available_gpu_types:
            if self.gpu_type_matches(available_gpu, requested_gpu_type):
                return  # GPU type is valid
        
        # GPU type not found - provide helpful error message
        sorted_gpu_types = sorted(list(available_gpu_types))
        
        # Group GPU types by vendor for better readability
        nvidia_gpus = [gpu for gpu in sorted_gpu_types if 'NVIDIA' in gpu.upper()]
        amd_gpus = [gpu for gpu in sorted_gpu_types if 'AMD' in gpu.upper() or 'RADEON' in gpu.upper()]
        other_gpus = [gpu for gpu in sorted_gpu_types if gpu not in nvidia_gpus and gpu not in amd_gpus]
        
        error_message = f"GPU type '{requested_gpu_type}' is not available in any cloud provider. "
        error_message += f"Available GPU types:\n\n"
        
        if nvidia_gpus:
            error_message += f"NVIDIA GPUs:\n"
            for gpu in nvidia_gpus:
                error_message += f"  - {gpu}\n"
            error_message += f"\n"
        
        if amd_gpus:
            error_message += f"AMD GPUs:\n"
            for gpu in amd_gpus:
                error_message += f"  - {gpu}\n"
            error_message += f"\n"
        
        if other_gpus:
            error_message += f"Other GPUs:\n"
            for gpu in other_gpus:
                error_message += f"  - {gpu}\n"
            error_message += f"\n"
        
        error_message += f"Consider using:\n"
        error_message += f"  - Exact GPU name from the list above\n"
        error_message += f"  - Short GPU name (e.g., 'T4', 'V100', 'A100')\n"
        error_message += f"  - Generic GPU size: gpu_small, gpu_medium, gpu_large\n"
        error_message += f"  - Remove gpu_type to find cheapest GPU regardless of type"
        
        raise ValueError(error_message)
    
    def get_cheapest_instance_type(self, instance, provider):
        """Get the specific instance type selected by cheapest provider analysis."""
        # Apply flavor-to-cores/memory conversion for cheapest provider
        instance = self._convert_flavor_to_specs_for_cheapest(instance)
        
        size = instance.get("flavor")  # Changed from "size" to "flavor"
        cores = instance.get("cores")
        memory = instance.get("memory")
        gpu_count = instance.get("gpu_count")
        
        if size:
            # Flavor-based selection
            provider_costs = self.find_cheapest_by_size(size)
        elif cores and memory:
            # Hardware specification-based selection
            provider_costs = self.find_cheapest_by_specs(cores, memory, gpu_count)
        else:
            # Fallback
            provider_costs = self.find_cheapest_by_size("medium")
        
        if provider in provider_costs:
            return provider_costs[provider]['instance_type']
        
        return None
    
    def resolve_instance_type(self, provider, flavor, instance):
        """Resolve instance type based on provider, flavor, and instance specifications."""
        instance_name = instance.get("name", "unnamed")
        
        # If flavor is already a provider-specific type, return it directly
        if flavor:
            # Check if this looks like a provider-specific instance type
            provider_prefixes = {
                'aws': ['t3.', 't2.', 't4g.', 'm5.', 'm5a.', 'm5ad.', 'm5d.', 'm5dn.', 'm5n.', 'm5zn.', 'm6a.', 'm6g.', 'm6gd.', 'm6i.', 'm6id.', 'm6idn.', 'm6in.', 'm7a.', 'm7g.', 'm7gd.', 'm7i.', 'm7i-flex.', 'c5.', 'c5a.', 'c5ad.', 'c5d.', 'c5n.', 'c6a.', 'c6g.', 'c6gd.', 'c6gn.', 'c6i.', 'c6id.', 'c6in.', 'c7a.', 'c7g.', 'c7gd.', 'c7gn.', 'c7i.', 'r5.', 'r5a.', 'r5ad.', 'r5b.', 'r5d.', 'r5dn.', 'r5n.', 'r6a.', 'r6g.', 'r6gd.', 'r6i.', 'r6id.', 'r6idn.', 'r6in.', 'r7a.', 'r7g.', 'r7gd.', 'r7i.', 'r7iz.', 'g4ad.', 'g4dn.', 'g5.', 'g5g.', 'p3.', 'p3dn.', 'p4d.', 'p4de.', 'p5.', 'inf1.', 'inf2.', 'trn1.', 'trn1n.', 'dl1.', 'f1.', 'i3.', 'i3en.', 'i4g.', 'i4i.', 'is4gen.', 'im4gn.', 'd2.', 'd3.', 'd3en.', 'h1.', 'z1d.', 'x1.', 'x1e.', 'x2gd.', 'x2idn.', 'x2iedn.', 'x2iezn.', 'u-3tb1.', 'u-6tb1.', 'u-9tb1.', 'u-12tb1.', 'u-18tb1.', 'u-24tb1.', 'u-1.', 'u-2.', 'u-3.', 'u-6.', 'u-9.', 'u-12.', 'u-18.', 'u-24.'],
                'azure': ['Standard_', 'Basic_'],
                'gcp': ['n1-', 'n2-', 'n2d-', 'n4-', 'e2-', 'c2-', 'c2d-', 'c3-', 'c3d-', 't2d-', 't2a-', 'a2-', 'a3-', 'g2-', 'm1-', 'm2-', 'm3-'],
                'oci': ['VM.Standard', 'VM.DenseIO', 'VM.GPU', 'VM.Optimized', 'BM.Standard', 'BM.DenseIO', 'BM.GPU', 'BM.HPC'],
                'ibm_vpc': ['bx2-', 'cx2-', 'mx2-', 'gx2-', 'gx3-', 'vx2d-', 'ox2-', 'ux2d-'],
                'alibaba': ['ecs.', 'r6.', 'c6.', 'g6.', 'ebm.', 'scc.', 'scch.']
            }
            
            if provider in provider_prefixes:
                if any(flavor.startswith(prefix) for prefix in provider_prefixes[provider]):
                    return flavor
        
        # Map generic flavor to provider-specific instance type
        if flavor in self.flavors:
            generic_flavor = self.flavors[flavor]
            if provider in generic_flavor:
                instance_type = generic_flavor[provider]
                # Flavor mapping output will be handled in _display_instance_hourly_cost
                return instance_type
            else:
                # Check if this is a GPU flavor that doesn't support this provider
                if any(gpu_prefix in flavor for gpu_prefix in ['gpu_', 'gpu_t4_', 'gpu_v100_', 'gpu_a100_', 'gpu_amd_']):
                    available_providers = list(generic_flavor.keys())
                    
                    # Special handling for AMD GPUs
                    if 'gpu_amd_' in flavor:
                        raise ValueError(
                            f"AMD GPU flavor '{flavor}' is only available on AWS. "
                            f"Provider '{provider}' does not support AMD GPUs. "
                            f"Consider using:\n"
                            f"  - AWS provider: provider: 'aws'\n"
                            f"  - Different GPU type: gpu_t4_small, gpu_v100_small, etc.\n"
                            f"  - Cost optimization: provider: 'cheapest' with gpu_type: 'AMD Radeon Pro V520'"
                        )
                    else:
                        # General GPU flavor not available on provider
                        raise ValueError(
                            f"GPU flavor '{flavor}' is not available on provider '{provider}'. "
                            f"Available providers for {flavor}: {', '.join(available_providers)}. "
                            f"Consider using:\n"
                            f"  - Supported provider: {', '.join(available_providers)}\n"
                            f"  - Cost optimization: provider: 'cheapest'\n"
                            f"  - Hardware specification: cores + memory + gpus"
                        )
        
        # Check provider-specific flavor mappings
        provider_flavors = self.flavors.get(provider, {})
        flavor_mappings = provider_flavors.get('flavor_mappings', {})
        
        if flavor in flavor_mappings:
            flavor_options = flavor_mappings[flavor]
            # Return the first (usually cheapest) option
            if flavor_options:
                return next(iter(flavor_options.keys()))
        
        # No mapping found - fail with clear error message
        # Get available flavors from both generic and provider-specific mappings
        generic_flavors = list(self.flavors.keys()) if hasattr(self, 'flavors') else []
        provider_flavors_list = list(flavor_mappings.keys()) if flavor_mappings else []
        all_available_flavors = sorted(set(generic_flavors + provider_flavors_list))
        
        raise ValueError(
            f"Instance '{instance_name}': No mapping found for flavor '{flavor}' on provider '{provider}'. "
            f"Available flavors: {', '.join(all_available_flavors)}. "
            f"Check mappings/flavors/generic.yaml and mappings/flavors/{provider}.yaml for supported flavors."
        )
    
    def get_instance_cost_info(self, provider, instance_type, flavor, provider_flavors):
        """Get detailed cost and specification information for a specific instance type."""
        # Look in provider-specific flavor mappings
        flavor_mappings = provider_flavors.get('flavor_mappings', {})
        
        if flavor in flavor_mappings:
            flavor_options = flavor_mappings[flavor]
            # Find the instance type in the flavor category
            for type_name, type_info in flavor_options.items():
                if type_name == instance_type:
                    cost = type_info.get('hourly_cost')
                    if cost is not None:
                        return {
                            'cost': cost,
                            'instance_type': instance_type,
                            'vcpus': type_info.get('vcpus'),
                            'memory_gb': type_info.get('memory_gb')
                        }
        
        # If flavor is a direct instance type, search across all flavor categories
        if flavor == instance_type:
            for flavor_category, flavor_options in flavor_mappings.items():
                if instance_type in flavor_options:
                    type_info = flavor_options[instance_type]
                    cost = type_info.get('hourly_cost')
                    if cost is not None:
                        return {
                            'cost': cost,
                            'instance_type': instance_type,
                            'vcpus': type_info.get('vcpus'),
                            'memory_gb': type_info.get('memory_gb'),
                            'gpu_count': type_info.get('gpu_count'),
                            'gpu_type': type_info.get('gpu_type')
                        }
        
        # Check direct machine type mapping for GCP
        if 'machine_types' in provider_flavors and instance_type in provider_flavors['machine_types']:
            type_info = provider_flavors['machine_types'][instance_type]
            cost = type_info.get('hourly_cost')
            if cost is not None:
                return {
                    'cost': cost,
                    'instance_type': instance_type,
                    'vcpus': type_info.get('vcpus'),
                    'memory_gb': type_info.get('memory_gb')
                }
        
        return None

    def _display_instance_hourly_cost(self, instance_name, provider, instance_type, flavor, original_provider, instance):
        """Display the hourly cost for an instance."""
        # For cheapest provider instances, get the cost from the cheapest provider analysis
        if original_provider in ['cheapest', 'cheapest-gpu']:
            try:
                # Get instance-specific exclusions
                instance_exclusions = instance.get('exclude_providers', [])
                
                if original_provider == 'cheapest':
                    # Handle memory in either MB or GB format
                    memory_value = instance.get('memory', 4)
                    if memory_value < 100:  # Assume GB if less than 100
                        memory_mb = memory_value * 1024
                    else:  # Assume MB if 100 or greater
                        memory_mb = memory_value
                    
                    # Get cost information for all providers
                    provider_costs = self.find_cheapest_by_specs(
                        instance.get('cores', 2), 
                        memory_mb,
                        instance.get('gpu_count', 0),
                        instance.get('gpu_type'),
                        instance_exclusions
                    )
                else:  # cheapest-gpu
                    gpu_type = instance.get("gpu_type")
                    provider_costs = self.find_cheapest_gpu_by_specs(gpu_type, instance_exclusions)
                
                if provider_costs and provider in provider_costs:
                    selected_option = provider_costs[provider]
                    hourly_cost = selected_option['cost']
                    instance_type = selected_option['instance_type']
                    # Get and display region information for cheapest provider
                    region = instance.get('region') or instance.get('location', 'unspecified')
                    mapped_region = self.locations.get(region, {}).get(provider, region) if region != 'unspecified' else 'unspecified'
                    if region == mapped_region:
                        self.print_instance_output(instance_name, provider, f"Region: {region}")
                    else:
                        self.print_instance_output(instance_name, provider, f"Region: {region} ({mapped_region})")
                    # Show flavor mapping format: original_flavor (resolved_instance_type)
                    if flavor and flavor != instance_type:
                        self.print_instance_output(instance_name, provider, f"Flavor: {flavor} ({instance_type})")
                    else:
                        self.print_instance_output(instance_name, provider, f"Flavor: {instance_type}")
                    # Apply discount and format display
                    original_cost = hourly_cost
                    discounted_cost = self.apply_discount(hourly_cost, provider)
                    cost_display = self._format_cost_with_discount(provider, original_cost, discounted_cost)
                    self.print_instance_output(instance_name, provider, f"Hourly Cost: {cost_display}")
                    # Track the discounted cost for total calculation
                    self.instance_costs.append({
                        'instance_name': instance_name,
                        'provider': provider,
                        'cost': discounted_cost
                    })
                    return
            except Exception:
                pass  # Fall back to regular cost lookup
        

        
        # Special handling for CNV provider
        if provider == 'cnv':
            provider_flavors = self.flavors.get(provider, {})
            flavor_mappings = provider_flavors.get('flavor_mappings', {})
            
            # Get the actual flavor name (could be from flavor or cores/memory)
            actual_flavor = flavor
            if not actual_flavor:
                # If no flavor specified, determine from cores/memory
                cores = instance.get('cores')
                memory = instance.get('memory')
                if cores and memory:
                    # For cores/memory, we'll show the specs instead of a flavor name
                    self.print_instance_output(instance_name, provider, f"Provider: {provider}")
                    self.print_instance_output(instance_name, provider, f"Instance specs: {cores} vCPU, {memory}MB RAM")
                    self.print_instance_output(instance_name, provider, "Hourly Cost: $0.001 (minimal CNV cost)")
                    self.instance_costs.append({
                        'instance_name': instance_name,
                        'provider': provider,
                        'cost': 0.001
                    })
                    return
            
            # Look up cost for the flavor
            if actual_flavor in flavor_mappings:
                flavor_options = flavor_mappings[actual_flavor]
                # Get the first flavor option (e.g., 'cnv-small' for 'small')
                flavor_name = next(iter(flavor_options.keys()))
                flavor_config = flavor_options[flavor_name]
                
                hourly_cost = flavor_config.get('hourly_cost', 0.001)
                vcpus = flavor_config.get('vcpus', 1)
                memory_gb = flavor_config.get('memory_gb', 1)
                
                self.print_instance_output(instance_name, provider, f"Provider: {provider}")
                self.print_instance_output(instance_name, provider, f"Instance flavor: {flavor_name} ({vcpus} vCPU, {memory_gb}GB RAM)")
                self.print_instance_output(instance_name, provider, f"Hourly Cost: ${hourly_cost:.4f}")
                
                self.instance_costs.append({
                    'instance_name': instance_name,
                    'provider': provider,
                    'cost': hourly_cost
                })
                return
            else:
                self.print_instance_output(instance_name, provider, f"Provider: {provider}")
                self.print_instance_output(instance_name, provider, f"Instance flavor: {actual_flavor}")
                self.print_instance_output(instance_name, provider, "Hourly Cost: $0.001 (minimal CNV cost)")
                self.instance_costs.append({
                    'instance_name': instance_name,
                    'provider': provider,
                    'cost': 0.001
                })
                return
        
        # Regular cost lookup for non-cheapest providers or fallback
        # Show provider information for regular instances
        self.print_instance_output(instance_name, provider, f"Provider: {provider}")
        
        provider_flavors = self.flavors.get(provider, {})
        cost_info = self.get_instance_cost_info(provider, instance_type, flavor, provider_flavors)
        
        if cost_info and cost_info.get('cost') is not None:
            hourly_cost = cost_info['cost']
            # Get and display region information
            region = instance.get('region', 'unspecified')
            mapped_region = self.locations.get(region, {}).get(provider, region) if region != 'unspecified' else 'unspecified'
            if region == mapped_region:
                self.print_instance_output(instance_name, provider, f"Region: {region}")
            else:
                self.print_instance_output(instance_name, provider, f"Region: {region} ({mapped_region})")
            # Show flavor mapping format: original_flavor (resolved_instance_type)
            if flavor and flavor != instance_type:
                self.print_instance_output(instance_name, provider, f"Flavor: {flavor} ({instance_type})")
            else:
                self.print_instance_output(instance_name, provider, f"Flavor: {instance_type}")
            # Apply discount and format display
            original_cost = hourly_cost
            discounted_cost = self.apply_discount(hourly_cost, provider)
            cost_display = self._format_cost_with_discount(provider, original_cost, discounted_cost)
            self.print_instance_output(instance_name, provider, f"Hourly Cost: {cost_display}")
            # Track the discounted cost for total calculation
            self.instance_costs.append({
                'instance_name': instance_name,
                'provider': provider,
                'cost': discounted_cost
            })
        else:
            # Get and display region information even when cost is not available
            region = instance.get('region', 'unspecified')
            mapped_region = self.locations.get(region, {}).get(provider, region) if region != 'unspecified' else 'unspecified'
            if region == mapped_region:
                self.print_instance_output(instance_name, provider, f"Region: {region}")
            else:
                self.print_instance_output(instance_name, provider, f"Region: {region} ({mapped_region})")
            # Show flavor mapping format: original_flavor (resolved_instance_type)
            if flavor and flavor != instance_type:
                self.print_instance_output(instance_name, provider, f"Flavor: {flavor} ({instance_type})")
            else:
                self.print_instance_output(instance_name, provider, f"Flavor: {instance_type}")
            self.print_instance_output(instance_name, provider, "Hourly Cost: Cost information not available")

    def calculate_openshift_cluster_cost(self, cluster, cluster_type):
        """Calculate the hourly cost for an OpenShift cluster."""
        try:
            # Determine provider based on cluster type
            provider = None
            if cluster_type == 'rosa-classic' or cluster_type == 'rosa-hcp':
                provider = 'aws'
            elif cluster_type == 'aro':
                provider = 'azure'
            elif cluster_type == 'self-managed':
                provider = cluster.get('provider', 'aws')
            
            if not provider:
                return None
            
            # Check if cluster uses size-based configuration (modern approach)
            cluster_size = cluster.get('size')
            if cluster_size:
                # Use OpenShift provider to get cluster size configuration
                try:
                    size_config = self.openshift_provider.get_cluster_size_config(cluster_size, cluster_type, provider)
                    
                    # Get counts from size config, with user overrides
                    controlplane_count = cluster.get('controlplane_count', size_config.get('controlplane_count', 0))
                    worker_count = cluster.get('worker_count', size_config.get('worker_count', 0))
                    
                    # Resolve controlplane and worker sizes to actual machine types
                    controlplane_size = size_config.get('controlplane_size', '')
                    worker_size = size_config.get('worker_size', '')
                    
                    controlplane_machine_type = ''
                    worker_machine_type = ''
                    
                    if controlplane_size:
                        try:
                            controlplane_machine_type = self.openshift_provider.get_openshift_machine_type(provider, controlplane_size, 'controlplane')
                        except Exception:
                            pass
                            
                    if worker_size:
                        try:
                            worker_machine_type = self.openshift_provider.get_openshift_machine_type(provider, worker_size, 'worker')
                        except Exception:
                            pass
                    
                except Exception:
                    # Fall back to direct machine type approach if size config fails
                    controlplane_count = cluster.get('controlplane_count', 0)
                    worker_count = cluster.get('worker_count', 0)
                    controlplane_machine_type = cluster.get('controlplane_machine_type', '')
                    worker_machine_type = cluster.get('worker_machine_type', '')
            else:
                # Use direct machine type approach (legacy)
                controlplane_count = cluster.get('controlplane_count', 0)
                worker_count = cluster.get('worker_count', 0)
                controlplane_machine_type = cluster.get('controlplane_machine_type', '')
                worker_machine_type = cluster.get('worker_machine_type', '')
            
            # Calculate costs for different node types
            total_cost = 0.0
            
            # Try OpenShift-specific flavors first, then fall back to regular provider flavors
            openshift_flavor_key = f"openshift_{provider}"
            openshift_flavors = self.flavors.get(openshift_flavor_key, {})
            provider_flavors = self.flavors.get(provider, {})
            
            # Calculate controlplane node costs
            if controlplane_count > 0 and controlplane_machine_type:
                controlplane_cost = None
                
                # Search in OpenShift-specific flavor mappings first
                for size, instances in openshift_flavors.get('flavor_mappings', {}).items():
                    if controlplane_machine_type in instances:
                        controlplane_cost = instances[controlplane_machine_type]
                        break
                
                # Fall back to regular provider flavors if not found
                if not controlplane_cost:
                    for size, instances in provider_flavors.get('flavor_mappings', {}).items():
                        if controlplane_machine_type in instances:
                            controlplane_cost = instances[controlplane_machine_type]
                            break
                
                if controlplane_cost and controlplane_cost.get('hourly_cost'):
                    total_cost += controlplane_cost['hourly_cost'] * controlplane_count
            
            # Calculate worker node costs
            if worker_count > 0 and worker_machine_type:
                worker_cost = None
                
                # Search in OpenShift-specific flavor mappings first
                for size, instances in openshift_flavors.get('flavor_mappings', {}).items():
                    if worker_machine_type in instances:
                        worker_cost = instances[worker_machine_type]
                        break
                
                # Fall back to regular provider flavors if not found
                if not worker_cost:
                    for size, instances in provider_flavors.get('flavor_mappings', {}).items():
                        if worker_machine_type in instances:
                            worker_cost = instances[worker_machine_type]
                            break
                
                if worker_cost and worker_cost.get('hourly_cost'):
                    total_cost += worker_cost['hourly_cost'] * worker_count
            
            return total_cost
            
        except Exception:
            return None

    def display_openshift_cluster_cost(self, cluster_name, cluster_type, hourly_cost):
        """Display the hourly cost for an OpenShift cluster."""
        if hourly_cost is not None:
            print(f"  Cluster hourly cost: ${hourly_cost:.4f}")
            # Track the cost for total calculation
            self.openshift_costs.append({
                'cluster_name': cluster_name,
                'cluster_type': cluster_type,
                'cost': hourly_cost
            })
        else:
            print(f"  Cluster hourly cost: Cost information not available")

    def get_openshift_cluster_cost_string(self, cluster_name, cluster_type, hourly_cost):
        """Get the hourly cost string for an OpenShift cluster."""
        if hourly_cost is not None:
            # Track the cost for total calculation
            self.openshift_costs.append({
                'cluster_name': cluster_name,
                'cluster_type': cluster_type,
                'cost': hourly_cost
            })
            return f"     Cluster hourly cost: ${hourly_cost:.4f}"
        else:
            return f"     Cluster hourly cost: Cost information not available"

    def display_total_hourly_cost(self):
        """Display the total hourly cost for all instances and OpenShift clusters."""
        total_cost = 0.0
        
        # Add instance costs
        if self.instance_costs:
            instance_total = sum(cost_info['cost'] for cost_info in self.instance_costs)
            total_cost += instance_total
        
        # Add OpenShift cluster costs
        if self.openshift_costs:
            openshift_total = sum(cost_info['cost'] for cost_info in self.openshift_costs)
            total_cost += openshift_total
        
        if total_cost > 0:
            print(f"\nTotal hourly cost for all instances and clusters: ${total_cost:.4f}")

    def get_total_hourly_cost_string(self):
        """Get the total hourly cost string for all instances and OpenShift clusters."""
        total_cost = 0.0
        
        # Add instance costs
        if self.instance_costs:
            instance_total = sum(cost_info['cost'] for cost_info in self.instance_costs)
            total_cost += instance_total
        
        # Add OpenShift cluster costs
        if self.openshift_costs:
            openshift_total = sum(cost_info['cost'] for cost_info in self.openshift_costs)
            total_cost += openshift_total
        
        if total_cost > 0:
            return f"\nTotal hourly cost for all instances and clusters: ${total_cost:.4f}"
        return ""


    # Placeholder methods for provider delegation
    def generate_virtual_machine(self, instance, index, yaml_data, available_subnets=None, full_yaml_data=None, zone=None):
        """Generate virtual machine configuration using provider modules."""
        provider = instance.get("provider")
        if not provider:
            instance_name = instance.get("name", "unknown")
            raise ValueError(f"Instance '{instance_name}' must specify a 'provider'")

        clean_name, has_guid_placeholder = self.clean_name(instance.get("name", f"instance_{index}"))
        flavor = instance.get("flavor")
        instance_name = instance.get("name", "unnamed")
        
        # Store original provider before it gets changed
        original_provider = provider
        
        # Handle cheapest provider meta-provider first to get the actual provider
        selected_instance_type = None
        if provider == 'cheapest':
            # Convert generic flavor to cores/memory if needed
            instance = self._convert_flavor_to_specs_for_cheapest(instance)
            provider = self.find_cheapest_provider(instance, suppress_output=True)
            # Update the instance with the selected provider for consistency
            instance = instance.copy()
            instance['provider'] = provider
            
            # Get the selected instance type from cheapest provider analysis
            selected_instance_type = self.get_cheapest_instance_type(instance, provider)
        elif provider == 'cheapest-gpu':
            # Convert generic flavor to cores/memory if needed
            instance = self._convert_flavor_to_specs_for_cheapest(instance)
            provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
            # Update the instance with the selected provider for consistency
            instance = instance.copy()
            instance['provider'] = provider
            
            # Get the selected instance type from cheapest GPU provider analysis
            selected_instance_type = self.get_cheapest_gpu_instance_type(instance, provider)
        
        # Start instance section with the resolved provider
        self.start_instance_section(instance_name, provider)
        
        # Clear region cache to ensure instance-specific messages are shown
        # This allows resolve_instance_region to be called again during instance processing
        cache_key = f"{instance_name}_{provider}_{instance.get('region', '')}_{instance.get('location', '')}"
        if cache_key in self._region_cache:
            del self._region_cache[cache_key]
        
        # Call resolve_instance_region to show instance-specific messages
        self.resolve_instance_region(instance, provider)
        
        # Handle cheapest provider meta-provider output (without cost analysis yet)
        if original_provider in ['cheapest', 'cheapest-gpu']:
            if original_provider == 'cheapest':
                self.print_instance_output(instance_name, provider, f"Provider: cheapest ({provider})")
            else:
                self.print_instance_output(instance_name, provider, f"Provider: cheapest-gpu ({provider})")
        
        # Validate that we have enough information to determine instance type
        cores = instance.get("cores")
        memory = instance.get("memory")
        
        # For CNV provider, allow cores and memory as an alternative to flavor
        if provider == 'cnv':
            if not flavor and not selected_instance_type and not (cores and memory):
                raise ValueError(
                    f"Instance '{instance_name}': Must specify either:\n"
                    f"  - 'flavor': small, medium, large, xlarge, etc.\n"
                    f"  - 'cores' and 'memory': e.g., cores: 2, memory: 4096 (MB)"
                )
        else:
            if not flavor and not selected_instance_type:
                raise ValueError(
                    f"Instance '{instance_name}': Must specify either:\n"
                    f"  - 'flavor': generic (small, medium, large, xlarge) or provider-specific (t3.small, e2-micro)\n"
                    f"  - Use 'provider: cheapest' with requirements (cores, memory, gpu_type)"
                )
        
        # Determine instance type (priority: direct flavor specification > cheapest selection > flavor mapping)
        # Skip instance type resolution for CNV provider entirely
        if provider == 'cnv':
            instance_type = None  # CNV doesn't use instance types
        else:
            instance_type = selected_instance_type or self.resolve_instance_type(provider, flavor, instance)

        # Calculate and display hourly cost
        self._display_instance_hourly_cost(instance_name, provider, instance_type, flavor, original_provider, instance)

        # Now show cost analysis for cheapest providers (after all instance details)
        if original_provider in ['cheapest', 'cheapest-gpu']:
            # Create a fresh copy of the original instance for cost analysis
            original_instance = {
                'name': instance.get('name'),
                'provider': original_provider,
                'cores': instance.get('cores', 2),
                'memory': instance.get('memory', 4),
                'gpu_count': instance.get('gpu_count', 0),
                'gpu_type': instance.get('gpu_type'),
                'exclude_providers': instance.get('exclude_providers', [])
            }
            
            if original_provider == 'cheapest':
                self._print_cost_analysis_for_instance(original_instance, provider)
            else:  # cheapest-gpu
                self._print_gpu_cost_analysis_for_instance(original_instance, provider)

        # Use full_yaml_data if provided, otherwise fall back to yaml_data
        effective_yaml_data = full_yaml_data if full_yaml_data is not None else yaml_data

        if provider == 'aws':
            strategy_info = {'instance_type': instance_type, 'architecture': 'x86_64'}
            return self.get_aws_provider().generate_aws_vm(instance, index, clean_name, strategy_info, available_subnets, effective_yaml_data, has_guid_placeholder)
        elif provider == 'azure':
            # For cheapest providers, pass the selected instance type instead of flavor
            azure_flavor = selected_instance_type or flavor
            return self.azure_provider.generate_azure_vm(instance, index, clean_name, azure_flavor, available_subnets, effective_yaml_data, has_guid_placeholder)
        elif provider == 'gcp':
            # For cheapest providers, pass the selected instance type instead of flavor
            gcp_flavor = selected_instance_type or flavor
            return self.gcp_provider.generate_gcp_vm(instance, index, clean_name, gcp_flavor, available_subnets, effective_yaml_data, has_guid_placeholder)
        elif provider == 'oci':
            # For cheapest providers, pass the selected instance type instead of flavor
            oci_flavor = selected_instance_type or flavor
            return self.oci_provider.generate_oci_vm(instance, index, clean_name, oci_flavor, available_subnets, effective_yaml_data, has_guid_placeholder)
        elif provider == 'vmware':
            # For cheapest providers, pass the selected instance type instead of flavor
            vmware_flavor = selected_instance_type or flavor
            return self.vmware_provider.generate_vmware_vm(instance, index, clean_name, vmware_flavor, available_subnets, effective_yaml_data, has_guid_placeholder)
        elif provider == 'alibaba':
            # For cheapest providers, pass the selected instance type instead of flavor
            alibaba_flavor = selected_instance_type or flavor
            return self.alibaba_provider.generate_alibaba_vm(instance, index, clean_name, alibaba_flavor, available_subnets, effective_yaml_data, has_guid_placeholder)
        elif provider in ['ibm_vpc', 'ibm_classic']:
            # For cheapest providers, pass the selected instance type instead of flavor
            ibm_flavor = selected_instance_type or flavor
            if provider == 'ibm_vpc':
                return self.ibm_vpc_provider.generate_ibm_vpc_vm(instance, index, clean_name, ibm_flavor, effective_yaml_data, has_guid_placeholder, zone)
            else:
                return self.ibm_classic_provider.generate_ibm_classic_vm(instance, index, clean_name, ibm_flavor, effective_yaml_data, has_guid_placeholder)
        elif provider == 'cnv':
            # For CNV, pass flavor (which may be None if using cores/memory)
            # The CNV provider will handle cores/memory internally
            return self.cnv_provider.generate_cnv_vm(instance, index, clean_name, flavor, available_subnets, effective_yaml_data, has_guid_placeholder)
        else:
            raise ValueError(f"Unsupported provider: {provider}")



    def generate_native_security_group_rule(self, rule, provider):
        """Generate native security group rule data for specified provider."""
        # Parse rule data
        protocol = rule.get('protocol', '').lower()
        port_range = rule.get('port_range')
        direction = rule.get('direction', 'ingress')
        source = rule.get('source', '0.0.0.0/0')
        destination = rule.get('destination')
        
        # Validate required fields
        if not protocol:
            raise ValueError(f"Security group rule missing required 'protocol' field. Rule: {rule}")
        if not port_range:
            raise ValueError(f"Security group rule missing required 'port_range' field. Rule: {rule}")
        if not source:
            raise ValueError(f"Security group rule missing required 'source' field. Rule: {rule}")
        
        # Validate destination for egress rules
        if direction == 'egress' and not destination:
            raise ValueError(f"Security group egress rule missing required 'destination' field. Rule: {rule}")
        
        # Validate protocol
        valid_protocols = ['tcp', 'udp', 'icmp', 'icmpv6', 'all', 'ah', 'esp', 'gre', 'ipip']
        if protocol not in valid_protocols:
            raise ValueError(f"Invalid protocol '{protocol}'. Valid protocols: {', '.join(valid_protocols)}. Rule: {rule}")
        
        # Validate port_range format
        import re
        port_pattern = re.compile(r'^([0-9]+|[0-9]+-[0-9]+)$')
        if not port_pattern.match(port_range):
            raise ValueError(f"Invalid port_range '{port_range}'. Must be single port (e.g., '22') or range (e.g., '80-90'). Rule: {rule}")
        
        # Parse port_range to from_port and to_port
        if '-' in port_range:
            from_port, to_port = port_range.split('-', 1)
            from_port = int(from_port.strip())
            to_port = int(to_port.strip())
        else:
            # Single port becomes range
            from_port = int(port_range)
            to_port = from_port
        
        # Validate port numbers
        if from_port < 0 or from_port > 65535 or to_port < 0 or to_port > 65535:
            raise ValueError(f"Port numbers must be between 0 and 65535. Got: {from_port}-{to_port}. Rule: {rule}")
        if from_port > to_port:
            raise ValueError(f"Invalid port range: from_port ({from_port}) cannot be greater than to_port ({to_port}). Rule: {rule}")
        
        # Validate source format
        self._validate_security_group_source(source, provider, rule)
        
        # Validate destination format if provided
        if destination:
            self._validate_security_group_source(destination, provider, rule)
        
        # Handle source/destination
        if self._is_cidr_block(source):
            source_cidr_blocks = [source]
        else:
            # Provider-specific source (e.g., security group reference)
            source_cidr_blocks = [source]  # Will be handled differently by providers
        
        if destination and self._is_cidr_block(destination):
            destination_cidr_blocks = [destination]
        elif destination:
            # Provider-specific destination
            destination_cidr_blocks = [destination]
        else:
            destination_cidr_blocks = []

        # Convert direction for different providers
        if provider == 'azure':
            direction = "Inbound" if direction == 'ingress' else "Outbound"
        elif provider in ['gcp', 'oci']:
            direction = direction.upper()  # INGRESS/EGRESS
        # AWS, IBM VPC, Alibaba use lowercase ingress/egress (no conversion needed)

        # Convert protocol for different providers
        if provider == 'azure':
            if protocol == 'tcp':
                protocol = 'Tcp'
            elif protocol == 'udp':
                protocol = 'Udp'
            elif protocol == 'icmp':
                protocol = 'Icmp'
            elif protocol == 'icmpv6':
                protocol = 'Icmpv6'
            elif protocol == 'all':
                protocol = '*'
            # Other protocols remain as-is
        
        return {
            'direction': direction,
            'from_port': from_port,
            'to_port': to_port,
            'protocol': protocol,
            'source_cidr_blocks': source_cidr_blocks,
            'destination_cidr_blocks': destination_cidr_blocks,
            'source': source,  # Keep original source for provider-specific handling
            'destination': destination,  # Keep original destination for provider-specific handling
            'is_source_cidr': self._is_cidr_block(source),
            'is_destination_cidr': self._is_cidr_block(destination) if destination else True
        }
    
    def _is_cidr_block(self, source):
        """Check if source is a CIDR block."""
        import re
        cidr_pattern = re.compile(r'^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$|^([0-9a-fA-F:]+::?)+/[0-9]{1,3}$')
        return bool(cidr_pattern.match(source))
    
    def _validate_security_group_source(self, source, provider, rule):
        """Validate security group source based on provider capabilities."""
        # CIDR blocks are always supported
        if self._is_cidr_block(source):
            return
        
        # Provider-specific source validation
        if provider == 'cheapest':
            raise ValueError(f"Provider-specific source '{source}' not supported with 'cheapest' provider. Use CIDR blocks only. Rule: {rule}")
        
        # AWS security group references
        if provider == 'aws' and source.startswith('sg-'):
            return
        
        # GCP tag-based sources
        if provider == 'gcp' and source == 'tags':
            return
        
        # Azure application security groups (future support)
        if provider == 'azure' and source.startswith('asg-'):
            return
        
        # IBM VPC security group references (future support)
        if provider == 'ibm_vpc' and source.startswith('sg-'):
            return
        
        # If we get here, it's an unsupported source type for this provider
        raise ValueError(f"Unsupported source '{source}' for provider '{provider}'. Use CIDR blocks or provider-specific references. Rule: {rule}")

    def analyze_regional_security_groups(self, config):
        """Analyze which security groups are needed in which regions."""
        regional_sgs = {}
        
        # Convert security groups list to dictionary for easier lookup
        security_groups_raw = config.get('security_groups', [])
        security_groups = {}
        
        # Handle both list and dictionary formats
        if isinstance(security_groups_raw, list):
            for sg in security_groups_raw:
                if isinstance(sg, dict) and 'name' in sg:
                    security_groups[sg['name']] = sg
        elif isinstance(security_groups_raw, dict):
            security_groups = security_groups_raw

        # Analyze instances to determine SG regional requirements
        for instance in config.get('instances', []):
            provider = instance.get('provider')
            
            # Resolve meta providers to actual providers (same logic as networking analysis)
            if provider == 'cheapest':
                provider = self.find_cheapest_provider(instance, suppress_output=True)
            elif provider == 'cheapest-gpu':
                provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
            
            if provider in ['aws', 'azure', 'gcp', 'ibm_vpc', 'ibm_classic']:  # Regional providers
                region = self._resolve_instance_region_silent(instance, provider)

                # Create a provider-specific region key to handle multiple providers in same region
                region_key = f"{region}_{provider}" if provider in ['ibm_vpc', 'ibm_classic'] else region

                # Track which SGs are needed in this region
                if region_key not in regional_sgs:
                    regional_sgs[region_key] = {'provider': provider, 'region': region, 'security_groups': set()}

                for sg_name in instance.get('security_groups', []):
                    if sg_name in security_groups:
                        regional_sgs[region_key]['security_groups'].add(sg_name)

        return regional_sgs

    def generate_regional_security_groups(self, config):
        """Generate security groups for each region where they're needed."""
        regional_analysis = self.analyze_regional_security_groups(config)
        
        # Convert security groups list to dictionary for easier lookup (same as in analyze method)
        security_groups_raw = config.get('security_groups', [])
        security_groups = {}
        
        # Handle both list and dictionary formats
        if isinstance(security_groups_raw, list):
            for sg in security_groups_raw:
                if isinstance(sg, dict) and 'name' in sg:
                    security_groups[sg['name']] = sg
        elif isinstance(security_groups_raw, dict):
            security_groups = security_groups_raw
            
        sg_terraform = ""

        for region_key, region_data in regional_analysis.items():
            provider = region_data['provider']
            region = region_data.get('region', region_key)  # Use actual region, fallback to region_key

            # Check if any outbound rules exist in the configured security groups
            has_outbound_rules = False
            for sg_name in region_data['security_groups']:
                sg_config = security_groups[sg_name]
                rules = sg_config.get('rules', [])
                for rule in rules:
                    if rule.get('direction') == 'egress':
                        has_outbound_rules = True
                        break
                if has_outbound_rules:
                    break

            # Check if auto-create outbound SG is enabled for this provider
            auto_create_outbound = True  # Default to True
            if provider == 'ibm_vpc':
                ibm_vpc_config = config.get('yamlforge', {}).get('ibm_vpc', {})
                auto_create_outbound = ibm_vpc_config.get('auto_create_outbound_sg', True)
            elif provider == 'ibm_classic':
                ibm_classic_config = config.get('yamlforge', {}).get('ibm_classic', {})
                auto_create_outbound = ibm_classic_config.get('auto_create_outbound_sg', True)

            # Create automatic outbound security group if needed
            if auto_create_outbound and not has_outbound_rules and provider in ['ibm_vpc', 'ibm_classic']:
                self.print_provider_output(provider, f"No outbound security group rules found. Creating automatic outbound security group for {region}.")
                guid = self.get_validated_guid(config)
                auto_outbound_sg_name = f"auto-outbound-{region}-{guid}"
                auto_outbound_rules = [{
                    'direction': 'egress',
                    'protocol': 'all',
                    'port_range': '1-65535',
                    'destination': '0.0.0.0/0',
                    'description': 'Allow all outbound traffic (auto-created)'
                }]
                if provider == 'ibm_vpc':
                    sg_terraform += self.ibm_vpc_provider.generate_ibm_security_group(auto_outbound_sg_name, auto_outbound_rules, region, config)
                elif provider == 'ibm_classic':
                    sg_terraform += self.ibm_classic_provider.generate_ibm_classic_security_group(auto_outbound_sg_name, auto_outbound_rules, region, config)
                
                # Add the auto-created security group to all instances in this region
                for instance in config.get('instances', []):
                    instance_provider = instance.get('provider')
                    if instance_provider == 'cheapest':
                        instance_provider = self.find_cheapest_provider(instance, suppress_output=True)
                    elif instance_provider == 'cheapest-gpu':
                        instance_provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
                    
                    if instance_provider == 'ibm_vpc' and self._resolve_instance_region_silent(instance, 'ibm_vpc') == region:
                        if 'security_groups' not in instance:
                            instance['security_groups'] = []
                        instance['security_groups'].append(auto_outbound_sg_name)
                    elif instance_provider == 'ibm_classic' and self._resolve_instance_region_silent(instance, 'ibm_classic') == region:
                        if 'security_groups' not in instance:
                            instance['security_groups'] = []
                        instance['security_groups'].append(auto_outbound_sg_name)

            for sg_name in region_data['security_groups']:
                sg_config = security_groups[sg_name]
                rules = sg_config.get('rules', [])

                # Generate region-specific security group
                if provider == 'aws':
                    sg_terraform += self.get_aws_provider().generate_aws_security_group(sg_name, rules, region, config)
                elif provider == 'azure':
                    sg_terraform += self.azure_provider.generate_azure_security_group(sg_name, rules, region, config)
                elif provider == 'gcp':
                    sg_terraform += self.gcp_provider.generate_gcp_firewall_rules(sg_name, rules, region)
                elif provider == 'ibm_vpc':
                    sg_terraform += self.ibm_vpc_provider.generate_ibm_security_group(sg_name, rules, region, config)
                elif provider == 'ibm_classic':
                    sg_terraform += self.ibm_classic_provider.generate_ibm_classic_security_group(sg_name, rules, region, config)
                elif provider == 'oci':
                    sg_terraform += self.oci_provider.generate_oci_security_group(sg_name, rules, region, config)
                elif provider == 'alibaba':
                    sg_terraform += self.alibaba_provider.generate_alibaba_security_group(sg_name, rules, region, config)

        return sg_terraform

    def analyze_regional_instances(self, config):
        """Analyze which regions have instances deployed and need networking."""
        regional_instances = {}
        
        # Analyze instances to determine regional networking requirements
        for instance in config.get('instances', []):
            provider = instance.get('provider')
            
            # Resolve meta providers to actual providers
            if provider == 'cheapest':
                provider = self.find_cheapest_provider(instance, suppress_output=True)
            elif provider == 'cheapest-gpu':
                provider = self.find_cheapest_gpu_provider(instance, suppress_output=True)
            
            if provider in ['aws', 'azure', 'gcp', 'ibm_vpc', 'ibm_classic']:  # Regional providers
                region = self._resolve_instance_region_silent(instance, provider)

                # Create a provider-specific region key to handle multiple providers in same region
                region_key = f"{region}_{provider}" if provider in ['ibm_vpc', 'ibm_classic'] else region

                # Track regions that need networking
                if region_key not in regional_instances:
                    regional_instances[region_key] = {'provider': provider, 'region': region, 'instances': []}
                
                regional_instances[region_key]['instances'].append(instance)

        return regional_instances

    def collect_ibm_vpc_zones(self, config):
        """Collect zone information for IBM VPC instances to ensure consistency."""
        instances = config.get('instances', [])
        zone_map = {}  # region -> zone
        
        for instance in instances:
            if instance.get('provider') == 'ibm_vpc':
                region = self._resolve_instance_region_silent(instance, 'ibm_vpc')
                user_specified_zone = instance.get('zone')
                
                if region not in zone_map:
                    # First instance in this region - validate/select zone
                    if user_specified_zone:
                        # Validate the zone
                        self.ibm_vpc_provider.validate_and_select_zone(region, user_specified_zone, config)
                        zone_map[region] = user_specified_zone
                    else:
                        # Auto-select zone
                        selected_zone = self.ibm_vpc_provider.validate_and_select_zone(region, None, config)
                        zone_map[region] = selected_zone
                else:
                    # Subsequent instance in this region - validate consistency
                    if user_specified_zone:
                        if user_specified_zone != zone_map[region]:
                            raise ValueError(f"Instance '{instance.get('name', 'unknown')}': Zone '{user_specified_zone}' conflicts with "
                                           f"previously selected zone '{zone_map[region]}' for region '{region}'. "
                                           f"All instances in the same region must use the same zone.")
                    # If no zone specified, use the already selected one
        
        return zone_map

    def generate_regional_networking(self, config, ibm_vpc_zones=None):
        """Generate networking infrastructure for each region where instances are deployed."""
        regional_analysis = self.analyze_regional_instances(config)
        networking_terraform = ""
        
        # Use provided zone information or collect it if not provided
        if ibm_vpc_zones is None:
            ibm_vpc_zones = self.collect_ibm_vpc_zones(config)

        if hasattr(self, 'verbose') and self.verbose:
            print(f"DEBUG: regional_analysis = {regional_analysis}")
            print(f"DEBUG: ibm_vpc_zones = {ibm_vpc_zones}")

        for region_key, region_data in regional_analysis.items():
            provider = region_data['provider']
            region = region_data.get('region', region_key)  # Use actual region, fallback to region_key
            if hasattr(self, 'verbose') and self.verbose:
                print(f"DEBUG: Processing region_key={region_key}, region={region}, provider={provider}")
            # Get deployment name from cloud_workspace
            cloud_workspace = config.get('cloud_workspace', {})
            workspace_name = cloud_workspace.get('name', 'yamlforge-deployment')
            
            # Check if the workspace name already contains a GUID placeholder
            if '{guid}' in workspace_name:
                # If it already has {guid}, just replace it without appending another GUID
                deployment_name = self.replace_guid_placeholders(workspace_name)
            else:
                # If it doesn't have {guid}, append the GUID
                deployment_name = f"{workspace_name}-{self.get_validated_guid(config)}"
            deployment_config = config.get('network', {})

            # Generate regional networking
            if provider == 'aws':
                networking_terraform += self.get_aws_provider().generate_aws_networking(deployment_name, deployment_config, region, config)
            elif provider == 'azure':
                networking_terraform += self.azure_provider.generate_azure_networking(deployment_name, deployment_config, region, config)
            elif provider == 'gcp':
                networking_terraform += self.gcp_provider.generate_gcp_networking(deployment_name, deployment_config, region)
            elif provider == 'ibm_vpc':
                if hasattr(self, 'verbose') and self.verbose:
                    print(f"DEBUG: About to call IBM VPC networking for region={region}")
                # Pass the selected zone for this region
                zone = ibm_vpc_zones.get(region)
                networking_terraform += self.ibm_vpc_provider.generate_ibm_vpc_networking(deployment_name, deployment_config, region, config, zone)
            elif provider == 'ibm_classic':
                networking_terraform += self.ibm_classic_provider.generate_ibm_classic_networking(deployment_name, deployment_config, region, config)
            elif provider == 'oci':
                networking_terraform += self.oci_provider.generate_oci_networking(deployment_name, deployment_config, region, config)
            elif provider == 'vmware':
                networking_terraform += self.vmware_provider.generate_vmware_networking(deployment_name, deployment_config, region, config)
            elif provider == 'alibaba':
                networking_terraform += self.alibaba_provider.generate_alibaba_networking(deployment_name, deployment_config, region, config)

        return networking_terraform

    def get_instance_gcp_firewall_refs(self, instance):
        """Get GCP firewall tag references for an instance."""
        # For GCP, security groups become firewall tags
        sg_names = instance.get('security_groups', [])
        resolved_tags = []
        for sg_name in sg_names:
            # Replace {guid} placeholder in security group name
            resolved_name = self.replace_guid_placeholders(sg_name)
            # GCP tags must be lowercase and contain only letters, numbers, and dashes
            sanitized_name = resolved_name.lower().replace("_", "-")
            resolved_tags.append(sanitized_name)
        return resolved_tags

    def extract_rhel_info(self, image_key):
        """Extract RHEL version and architecture from image key."""
        import re
        
        # Default values
        default_version = "9"
        default_arch = "x86_64"
        
        # Try to extract RHEL version from the image key
        # Patterns to match: RHEL-9, RHEL-10.0, RHEL-8.9, RHEL9, RHEL10, etc.
        rhel_patterns = [
            r'RHEL-?(\d+(?:\.\d+)?)',  # RHEL-10.0, RHEL-9, RHEL9, etc.
            r'rhel-?(\d+(?:\.\d+)?)',  # rhel-10.0, rhel-9, rhel9, etc.
        ]
        
        version = default_version
        for pattern in rhel_patterns:
            match = re.search(pattern, image_key, re.IGNORECASE)
            if match:
                version = match.group(1)
                break
        
        # Try to extract architecture from the image key
        # Common patterns: x86_64, amd64, arm64, aarch64
        arch_patterns = [
            r'(x86_64|amd64)',     # x86_64 or amd64
            r'(arm64|aarch64)',    # ARM 64-bit
        ]
        
        architecture = default_arch
        for pattern in arch_patterns:
            match = re.search(pattern, image_key, re.IGNORECASE)
            if match:
                architecture = match.group(1)
                # Normalize architecture names
                if architecture.lower() in ['amd64']:
                    architecture = 'x86_64'
                elif architecture.lower() in ['aarch64']:
                    architecture = 'arm64'
                break
        
        return version, architecture

    def extract_fedora_version(self, image_key):
        """Extract Fedora version from image key."""
        import re
        
        # Default version
        default_version = "39"
        
        # Try to extract Fedora version from the image key
        # Patterns to match: Fedora-39, Fedora39, FEDORA-40, etc.
        fedora_patterns = [
            r'fedora-?(\d+)',  # fedora-39, fedora39, etc.
            r'FEDORA-?(\d+)',  # FEDORA-39, FEDORA39, etc.
        ]
        
        for pattern in fedora_patterns:
            match = re.search(pattern, image_key)
            if match:
                return match.group(1)
        
        return default_version

    def generate_rhel_pattern_config(self, image_key):
        """Generate RHEL pattern configuration for dynamic discovery."""
        # Extract RHEL info to generate appropriate pattern
        rhel_version, architecture = self.extract_rhel_info(image_key)
        
        # Determine if this is a GOLD/BYOS image
        is_gold = "GOLD" in image_key.upper() or "BYOS" in image_key.upper()
        
        if is_gold:
            name_pattern = f"RHEL-{rhel_version}*_HVM*Access*"
            owner_key = "redhat_gold"
        else:
            name_pattern = f"RHEL-{rhel_version}*_HVM*"
            owner_key = "redhat_public"
        
        return {
            'name_pattern': name_pattern,
            'owner_key': owner_key,
            'architecture': architecture
        }

    def determine_default_owner_key(self, image_key):
        """Determine default owner key for image discovery."""
        return "redhat_public"

    def generate_ami_data_source(self, image_key, instance_name, architecture):
        """Generate AWS AMI data source."""
        clean_name = self.clean_name(instance_name)
        
        # Handle different image types
        if "FEDORA" in image_key.upper():
            # Fedora images
            fedora_version = self.extract_fedora_version(image_key)
            owner = "125523088429"  # Fedora project account
            name_pattern = f"Fedora-Cloud-Base-{fedora_version}*"
            arch = "x86_64"  # Default architecture for Fedora
            is_gold = False
        else:
            # RHEL images (default)
            rhel_version, arch = self.extract_rhel_info(image_key)
            is_gold = "GOLD" in image_key.upper() or "BYOS" in image_key.upper()
            
            # Determine owner based on image type
            if is_gold:
                owner = "309956199498"  # Red Hat Gold account
                # Use pattern that targets latest versions (e.g., RHEL-9.6.*_HVM.*Access*)
                # This will naturally select the highest version due to lexicographic sorting
                name_pattern = f"RHEL-{rhel_version}.*_HVM.*Access*"
            else:
                owner = "309956199498"  # Red Hat public account
                # Use pattern that targets latest versions (e.g., RHEL-9.6.*_HVM*)
                # This will naturally select the highest version due to lexicographic sorting
                name_pattern = f"RHEL-{rhel_version}.*_HVM*"
        
        # Generate the data source
        data_source = f'''
# AWS AMI Data Source for {image_key}
data "aws_ami" "{clean_name}_ami" {{
  most_recent = true
  owners      = ["{owner}"]

  filter {{
    name   = "name"
    values = ["{name_pattern}"]
  }}

  filter {{
    name   = "virtualization-type"
    values = ["hvm"]
  }}

  filter {{
    name   = "architecture"
    values = ["{arch}"]
  }}

  filter {{
    name   = "root-device-type"
    values = ["ebs"]
  }}'''

        # Add is-public filter for GOLD images
        if is_gold:
            data_source += f'''

  filter {{
    name   = "is-public"
    values = ["false"]
  }}'''

        data_source += f'''
}}
'''
        return data_source
