# ssh_connection_builder.py
"""
Unified SSH connection builder that uses SSH config as primary source of truth.

This module provides a single, consistent way to build SSH commands for all
components (terminal, SCP, SFTP, ssh-copy-id) that matches default SSH behavior.
"""
import os
import logging
import subprocess
from typing import Dict, List, Optional, Tuple, Union
from dataclasses import dataclass

from .ssh_config_utils import get_effective_ssh_config
from .askpass_utils import (
    get_ssh_env_with_askpass,
    ensure_key_in_agent,
    lookup_passphrase,
)
from .ssh_password_exec import run_ssh_with_password, run_scp_with_password

logger = logging.getLogger(__name__)


@dataclass
class SSHConnectionCommand:
    """Represents a prepared SSH connection command."""
    command: List[str]  # The SSH command to execute
    env: Dict[str, str]  # Environment variables
    use_sshpass: bool = False  # Whether to use sshpass
    password: Optional[str] = None  # Password for sshpass (if needed)
    use_askpass: bool = False  # Whether to use SSH_ASKPASS


@dataclass
class ConnectionContext:
    """Context for building SSH connections."""
    connection: any  # Connection object
    connection_manager: Optional[any] = None  # ConnectionManager instance
    config: Optional[any] = None  # Config instance
    command_type: str = 'ssh'  # 'ssh', 'scp', 'sftp', 'ssh-copy-id'
    extra_args: List[str] = None  # Extra arguments to append
    # Advanced features
    port_forwarding_rules: Optional[List[Dict]] = None  # Port forwarding rules
    remote_command: Optional[str] = None  # Remote command to execute
    local_command: Optional[str] = None  # Local command to execute
    extra_ssh_config: Optional[str] = None  # Extra SSH config options (from advanced tab)
    known_hosts_path: Optional[str] = None  # Custom known hosts file
    native_mode: bool = False  # Use native SSH mode (minimal command)
    quick_connect_mode: bool = False  # Use quick connect command
    quick_connect_command: Optional[str] = None  # Quick connect command string


def _get_ssh_config_value(
    config: Dict[str, Union[str, List[str]]],
    key: str,
    default: Optional[str] = None
) -> Optional[str]:
    """Get a value from SSH config, handling both single values and lists."""
    value = config.get(key.lower())
    if value is None:
        return default
    if isinstance(value, list):
        return value[-1] if value else default
    return value


def _get_ssh_config_list(
    config: Dict[str, Union[str, List[str]]],
    key: str
) -> List[str]:
    """Get a list value from SSH config."""
    value = config.get(key.lower())
    if value is None:
        return []
    if isinstance(value, list):
        return value
    return [value] if value else []


def _should_add_keys_to_agent(config: Dict[str, Union[str, List[str]]]) -> bool:
    """Check if AddKeysToAgent directive requires adding keys to agent."""
    add_keys = _get_ssh_config_value(config, 'addkeystoagent', '').lower()
    return add_keys in ('yes', 'ask', 'confirm')


def _is_identity_agent_disabled(config: Dict[str, Union[str, List[str]]]) -> bool:
    """Check if IdentityAgent is disabled."""
    identity_agent = _get_ssh_config_value(config, 'identityagent', '').lower()
    return identity_agent == 'none'


def _prepare_key_for_connection(
    key_path: str,
    config: Dict[str, Union[str, List[str]]],
    connection_manager: Optional[any] = None
) -> bool:
    """
    Prepare SSH key for connection.
    Only adds to agent if AddKeysToAgent is enabled in SSH config.
    """
    if not key_path or not os.path.isfile(key_path):
        return False
    
    # Check if IdentityAgent is disabled
    if _is_identity_agent_disabled(config):
        logger.debug(f"IdentityAgent disabled; skipping key preparation: {key_path}")
        return False
    
    # Check if AddKeysToAgent requires adding to agent
    if not _should_add_keys_to_agent(config):
        logger.debug(f"AddKeysToAgent not enabled; skipping key preparation (default SSH behavior): {key_path}")
        return False
    
    # Add key to agent
    try:
        if connection_manager and hasattr(connection_manager, 'prepare_key_for_connection'):
            return connection_manager.prepare_key_for_connection(key_path)
        else:
            return ensure_key_in_agent(key_path)
    except Exception as e:
        logger.warning(f"Error preparing key for connection: {e}")
        return False


def _get_stored_password(
    connection: any,
    connection_manager: Optional[any] = None
) -> Optional[str]:
    """Get stored password for connection.
    
    Checks in this order:
    1. Connection object's password attribute (in-memory, from dialog)
    2. Connection manager's stored password (libsecret/keyring/keychain)
    """
    # First check connection object for in-memory password
    try:
        in_memory_password = getattr(connection, 'password', None)
        if in_memory_password:
            return in_memory_password
    except Exception:
        pass
    
    # Then check connection manager storage
    if not connection_manager:
        return None
    
    try:
        host = getattr(connection, 'hostname', '') or getattr(connection, 'host', '')
        username = getattr(connection, 'username', '')
        if host and username:
            stored = connection_manager.get_password(host, username)
            if stored:
                return stored
    except Exception:
        pass
    return None


def _get_stored_passphrase(
    key_path: str,
    connection_manager: Optional[any] = None
) -> Optional[str]:
    """Get stored passphrase for key."""
    if not key_path:
        return None
    
    # Try connection_manager first if available
    if connection_manager:
        try:
            if hasattr(connection_manager, 'get_key_passphrase'):
                result = connection_manager.get_key_passphrase(key_path)
                if result:
                    return result
        except Exception:
            pass
    
    # Fallback to direct lookup (works even without connection_manager)
    try:
        result = lookup_passphrase(key_path)
        if result:
            return result
    except Exception:
        pass
    
    return None


def _build_base_ssh_command(
    connection: any,
    config: Dict[str, Union[str, List[str]]],
    app_config: Optional[any] = None,
    command_type: str = 'ssh'
) -> List[str]:
    """
    Build base SSH command from SSH config and connection settings.
    This matches default SSH behavior as closely as possible.
    """
    # Determine the SSH binary
    if command_type == 'scp':
        cmd = ['scp']
    elif command_type == 'sftp':
        cmd = ['sftp']
    elif command_type == 'ssh-copy-id':
        cmd = ['ssh-copy-id']
    else:
        cmd = ['ssh']
    
    # Get app-level SSH settings
    app_ssh_config = {}
    if app_config:
        try:
            app_ssh_config = app_config.get_ssh_config() if hasattr(app_config, 'get_ssh_config') else {}
        except Exception:
            pass
    
    # Apply app-level overrides (verbosity, compression, etc.)
    verbosity = int(app_ssh_config.get('verbosity', 0) or 0)
    if verbosity > 0:
        for _ in range(min(verbosity, 3)):
            cmd.append('-v')
    
    compression = bool(app_ssh_config.get('compression', False))
    if compression:
        cmd.append('-C')
    
    # Apply connection timeout if specified
    connect_timeout = app_ssh_config.get('connection_timeout')
    if connect_timeout:
        try:
            timeout = int(connect_timeout)
            if timeout > 0:
                cmd.extend(['-o', f'ConnectTimeout={timeout}'])
        except (ValueError, TypeError):
            pass
    
    # Apply connection attempts if specified
    connection_attempts = app_ssh_config.get('connection_attempts')
    if connection_attempts:
        try:
            attempts = int(connection_attempts)
            if attempts > 0:
                cmd.extend(['-o', f'ConnectionAttempts={attempts}'])
        except (ValueError, TypeError):
            pass
    
    # Apply keepalive settings if specified
    keepalive_interval = app_ssh_config.get('keepalive_interval')
    if keepalive_interval:
        try:
            interval = int(keepalive_interval)
            if interval > 0:
                cmd.extend(['-o', f'ServerAliveInterval={interval}'])
        except (ValueError, TypeError):
            pass
    
    keepalive_count = app_ssh_config.get('keepalive_count_max')
    if keepalive_count:
        try:
            count = int(keepalive_count)
            if count > 0:
                cmd.extend(['-o', f'ServerAliveCountMax={count}'])
        except (ValueError, TypeError):
            pass
    
    # Apply strict host key checking
    strict_host = str(app_ssh_config.get('strict_host_key_checking', '') or '').strip()
    auto_add = bool(app_ssh_config.get('auto_add_host_keys', True))
    
    if strict_host:
        cmd.extend(['-o', f'StrictHostKeyChecking={strict_host}'])
    elif auto_add:
        cmd.extend(['-o', 'StrictHostKeyChecking=accept-new'])
    
    # Apply port if specified and not default
    port = getattr(connection, 'port', None)
    if port and port != 22:
        if command_type == 'scp':
            cmd.extend(['-P', str(port)])
        else:
            cmd.extend(['-p', str(port)])
    
    # Apply IdentityFile from SSH config (SSH config is primary source)
    identity_files = _get_ssh_config_list(config, 'identityfile')
    for identity_file in identity_files:
        if identity_file and os.path.isfile(os.path.expanduser(identity_file)):
            cmd.extend(['-i', os.path.expanduser(identity_file)])
            # If IdentitiesOnly is set, only use this key
            if _get_ssh_config_value(config, 'identitiesonly', '').lower() in ('yes', 'true', '1'):
                cmd.extend(['-o', 'IdentitiesOnly=yes'])
                break  # Only first key when IdentitiesOnly is set
    
    # Apply CertificateFile if specified
    cert_file = _get_ssh_config_value(config, 'certificatefile')
    if cert_file and cert_file.strip() and os.path.isfile(os.path.expanduser(cert_file)):
        cmd.extend(['-o', f'CertificateFile={cert_file}'])
    
    # Apply ProxyCommand/ProxyJump if specified
    proxy_command = _get_ssh_config_value(config, 'proxycommand')
    if proxy_command and proxy_command.strip():
        cmd.extend(['-o', f'ProxyCommand={proxy_command}'])
    
    proxy_jump = _get_ssh_config_list(config, 'proxyjump')
    if proxy_jump:
        # Filter out empty values
        proxy_jump_filtered = [j for j in proxy_jump if j and j.strip()]
        if proxy_jump_filtered:
            cmd.extend(['-o', f'ProxyJump={",".join(proxy_jump_filtered)}'])
    
    # Apply X11 forwarding if enabled
    forward_x11 = _get_ssh_config_value(config, 'forwardx11', '').lower()
    if forward_x11 in ('yes', 'true', '1'):
        cmd.append('-X')
    
    # Apply other SSH config options that should be passed through
    # (SSH config is primary source, so we trust it)
    # Note: We don't override PreferredAuthentications here - let SSH config handle it
    
    return cmd


def build_ssh_connection(
    ctx: ConnectionContext
) -> SSHConnectionCommand:
    """
    Build a complete SSH connection command following the `ssh Host` pattern.
    
    This is the unified entry point for all SSH operations:
    - Terminal connections
    - SCP transfers
    - SFTP connections
    - ssh-copy-id operations
    
    Features:
    - Gets effective config from the right SSH config file (supports isolated mode)
    - Builds SSH command following `ssh Host` pattern (uses host identifier from SSH config)
    - Supports password and key-based authentication
    - Retrieves passwords from libsecret/keyring/keychain
    - Retrieves passphrases from libsecret/keyring/keychain
    - Automatic password authentication with sshpass
    - Automatic key-based authentication with askpass for passphrase-protected keys
    - Supports appending extra SSH options
    - Shows graphical prompts for passwords and passphrases via askpass
    
    Args:
        ctx: Connection context with connection, manager, config, etc.
    
    Returns:
        SSHConnectionCommand with command, env, and authentication info
    """
    connection = ctx.connection
    connection_manager = ctx.connection_manager
    app_config = ctx.config
    command_type = ctx.command_type
    extra_args = ctx.extra_args or []
    
    logger.debug(f"build_ssh_connection called: native_mode={ctx.native_mode}, quick_connect={ctx.quick_connect_mode}, connection_manager={'present' if connection_manager else 'None'}")
    
    # 1. Get effective SSH config from the right SSH config file
    # Try to get host identifier from connection
    host_label = getattr(connection, 'nickname', '') or \
                 getattr(connection, 'host', '') or \
                 getattr(connection, 'hostname', '')
    
    # If connection has resolve_host_identifier method, use it
    if not host_label and hasattr(connection, 'resolve_host_identifier'):
        try:
            host_label = connection.resolve_host_identifier()
        except Exception:
            pass
    
    if not host_label:
        raise ValueError("Connection must have a host identifier (nickname, host, or hostname)")
    
    # Check for config override (isolated mode, etc.)
    config_override = None
    if hasattr(connection, '_resolve_config_override_path'):
        try:
            config_override = connection._resolve_config_override_path()
        except Exception:
            pass
    
    # Handle native mode early - minimal SSH command
    if ctx.native_mode:
        # Native mode: just use ssh with host_label, let SSH config handle everything
        base_cmd = ['ssh']
        config_override = None
        if hasattr(connection, '_resolve_config_override_path'):
            try:
                config_override = connection._resolve_config_override_path()
            except Exception:
                pass
        if config_override:
            base_cmd.extend(['-F', config_override])
        # Add any SSH overrides from app config
        if app_config:
            try:
                app_ssh_config = app_config.get_ssh_config() if hasattr(app_config, 'get_ssh_config') else {}
                overrides = app_ssh_config.get('ssh_overrides', [])
                if isinstance(overrides, (list, tuple)):
                    for entry in overrides:
                        if entry:
                            base_cmd.append(str(entry))
            except Exception:
                pass
        base_cmd.append(host_label)
        # Use default environment (no askpass, no sshpass)
        env = os.environ.copy()
        env.pop('SSH_ASKPASS', None)
        env.pop('SSH_ASKPASS_REQUIRE', None)
        return SSHConnectionCommand(
            command=base_cmd,
            env=env,
            use_sshpass=False,
            password=None,
            use_askpass=False
        )
    
    # Handle quick connect mode early
    if ctx.quick_connect_mode and ctx.quick_connect_command:
        import shlex
        try:
            base_cmd = shlex.split(ctx.quick_connect_command)
        except ValueError:
            base_cmd = ctx.quick_connect_command.split()
        # Use default environment
        env = os.environ.copy()
        env.pop('SSH_ASKPASS', None)
        env.pop('SSH_ASKPASS_REQUIRE', None)
        return SSHConnectionCommand(
            command=base_cmd,
            env=env,
            use_sshpass=False,
            password=None,
            use_askpass=False
        )
    
    # Get effective SSH config for the host identifier
    try:
        if config_override:
            effective_config = get_effective_ssh_config(host_label, config_file=config_override)
            logger.debug(f"Using SSH config override: {config_override}")
        else:
            effective_config = get_effective_ssh_config(host_label)
            logger.debug(f"Using default SSH config for host: {host_label}")
    except Exception as e:
        logger.warning(f"Failed to get effective SSH config for {host_label}: {e}")
        effective_config = {}
    
    # 2. Build base SSH command following `ssh Host` pattern
    base_cmd = _build_base_ssh_command(connection, effective_config, app_config, command_type)
    
    # 3. Determine authentication method (password vs key-based)
    auth_method = int(getattr(connection, 'auth_method', 0) or 0)
    password_auth_selected = (auth_method == 1)
    
    # 4. Check for stored password (retrieved from libsecret/keyring/keychain)
    stored_password = _get_stored_password(connection, connection_manager)
    has_stored_password = bool(stored_password)
    if has_stored_password:
        logger.debug(f"Found stored password for {host_label}")
    
    # Determine if we need password authentication
    use_password_auth = password_auth_selected or (has_stored_password and not password_auth_selected)
    
    # Handle key-based authentication
    use_askpass = False
    has_saved_passphrase = False
    key_path = None
    
    if not password_auth_selected:
        # Get key file from connection (if explicitly set) or SSH config
        key_file = getattr(connection, 'keyfile', '') or ''
        key_select_mode = int(getattr(connection, 'key_select_mode', 0) or 0)
        
        # Determine if we need askpass based on IdentityAgent and key preparation
        identity_agent_disabled = _is_identity_agent_disabled(effective_config)
        key_prepared = False
        
        # If connection has explicit key file and key_select_mode is set, use it
        if key_file and not key_file.startswith('Select key file') and key_select_mode in (1, 2):
            # Connection explicitly specifies a key - add it to command if not already there
            key_path = os.path.expanduser(key_file)
            if os.path.isfile(key_path):
                # Check if key is already in base_cmd from SSH config
                identity_files = _get_ssh_config_list(effective_config, 'identityfile')
                key_already_added = any(
                    os.path.samefile(os.path.expanduser(f), key_path) 
                    if os.path.exists(os.path.expanduser(f)) else False 
                    for f in identity_files
                )
                
                if not key_already_added:
                    base_cmd.extend(['-i', key_path])
                    if key_select_mode == 1:
                        base_cmd.extend(['-o', 'IdentitiesOnly=yes'])
                
                # 5. Check for stored passphrase (retrieved from libsecret/keyring/keychain)
                # First check connection object for in-memory passphrase
                in_memory_passphrase = getattr(connection, 'key_passphrase', '') or ''
                if in_memory_passphrase:
                    has_saved_passphrase = True
                    logger.info(f"Found in-memory passphrase for key: {key_path}")
                else:
                    # Then check storage
                    logger.debug(f"Checking for stored passphrase for key: {key_path}, connection_manager: {connection_manager is not None}")
                    stored_passphrase = _get_stored_passphrase(key_path, connection_manager)
                    has_saved_passphrase = bool(stored_passphrase)
                    if has_saved_passphrase:
                        logger.info(f"Found stored passphrase for key: {key_path}")
                    else:
                        logger.debug(f"No stored passphrase found for key: {key_path}")
                
                # Prepare key (add to agent if AddKeysToAgent is set)
                key_prepared = _prepare_key_for_connection(key_path, effective_config, connection_manager)
        else:
            # Use SSH config keys or default behavior
            identity_files = _get_ssh_config_list(effective_config, 'identityfile')
            if identity_files:
                # Try to prepare first identity file
                first_key = os.path.expanduser(identity_files[0])
                if os.path.isfile(first_key):
                    key_path = first_key
                    # 5. Check for stored passphrase (retrieved from libsecret/keyring/keychain)
                    # First check connection object for in-memory passphrase
                    in_memory_passphrase = getattr(connection, 'key_passphrase', '') or ''
                    if in_memory_passphrase:
                        has_saved_passphrase = True
                        logger.info(f"Found in-memory passphrase for key: {key_path}")
                    else:
                        # Then check storage
                        logger.debug(f"Checking for stored passphrase for SSH config key: {key_path}, connection_manager: {connection_manager is not None}")
                        stored_passphrase = _get_stored_passphrase(key_path, connection_manager)
                        has_saved_passphrase = bool(stored_passphrase)
                        if has_saved_passphrase:
                            logger.info(f"Found stored passphrase for SSH config key: {key_path}")
                        else:
                            logger.debug(f"No stored passphrase found for SSH config key: {key_path}")
                    key_prepared = _prepare_key_for_connection(first_key, effective_config, connection_manager)
        
        # 7. Set up askpass for automatic key-based authentication with passphrase-protected keys
        # The askpass script will:
        # - First try to retrieve passphrase from storage (libsecret/keyring/keychain)
        # - If not found, show graphical prompt for passphrase
        
        # Check for passphrases in all possible keys (even if key_path isn't explicitly set)
        # This handles cases where SSH config specifies keys or default keys are used
        if not has_saved_passphrase and not password_auth_selected:
            # Try to find passphrases for keys that might be used
            identity_files = _get_ssh_config_list(effective_config, 'identityfile')
            if identity_files:
                for identity_file in identity_files:
                    expanded_identity = os.path.expanduser(identity_file)
                    if os.path.isfile(expanded_identity):
                        logger.debug(f"Checking for stored passphrase for SSH config identity file: {expanded_identity}")
                        stored_passphrase = _get_stored_passphrase(expanded_identity, connection_manager)
                        if stored_passphrase:
                            has_saved_passphrase = True
                            logger.info(f"Found stored passphrase for SSH config key: {expanded_identity}")
                            break
            # Also check default key locations if no explicit keys found
            if not has_saved_passphrase:
                default_keys = [
                    os.path.expanduser('~/.ssh/id_rsa'),
                    os.path.expanduser('~/.ssh/id_ed25519'),
                    os.path.expanduser('~/.ssh/id_ecdsa'),
                    os.path.expanduser('~/.ssh/id_dsa'),
                ]
                for default_key in default_keys:
                    if os.path.isfile(default_key):
                        logger.debug(f"Checking for stored passphrase for default key: {default_key}")
                        stored_passphrase = _get_stored_passphrase(default_key, connection_manager)
                        if stored_passphrase:
                            has_saved_passphrase = True
                            logger.info(f"Found stored passphrase for default key: {default_key}")
                            break
        
        if identity_agent_disabled:
            # IdentityAgent disabled - must use askpass to supply passphrase
            env = get_ssh_env_with_askpass(require="force")
            use_askpass = True
            logger.debug("IdentityAgent disabled - using forced askpass for passphrase")
        elif key_path and os.path.isfile(key_path):
            # Key exists - use askpass in prefer mode
            # SSH will use agent if key is there, askpass if it needs the passphrase
            # The askpass script will retrieve from storage or show GUI dialog
            env = get_ssh_env_with_askpass(require="prefer")
            use_askpass = True
            logger.debug(f"Key-based auth with askpass support for key: {key_path}")
        elif has_saved_passphrase:
            # We have a saved passphrase but no explicit key - still set up askpass
            # This handles cases where SSH config specifies keys
            env = get_ssh_env_with_askpass(require="prefer")
            use_askpass = True
            logger.debug("Saved passphrase found - using askpass")
        elif not password_auth_selected:
            # Key-based auth but no explicit passphrase found - still use askpass
            # The askpass script will show GUI dialog if needed
            env = get_ssh_env_with_askpass(require="prefer")
            use_askpass = True
            logger.info("Key-based auth - using askpass for potential passphrase prompts")
        else:
            # Password authentication selected - use default SSH behavior
            env = os.environ.copy()
            logger.debug("Password authentication selected - not using askpass")
    else:
        # Password authentication selected
        env = os.environ.copy()
        # Remove askpass so SSH can prompt for password if needed
        env.pop('SSH_ASKPASS', None)
        env.pop('SSH_ASKPASS_REQUIRE', None)
    
    # 2. Add host to command following `ssh Host` pattern
    # Use host_label (from SSH config) as the target, which follows the `ssh Host` pattern
    # SSH will resolve the actual hostname, user, port, etc. from the SSH config
    username = getattr(connection, 'username', '')
    hostname = getattr(connection, 'hostname', '') or getattr(connection, 'host', '')
    
    if command_type == 'scp':
        # SCP command format is handled by caller (they add sources/destinations)
        # But we can add the host identifier if needed
        pass
    elif command_type == 'ssh-copy-id':
        # ssh-copy-id format: ssh-copy-id [options] user@host
        # Prefer user@hostname if available, otherwise use host_label
        if username and hostname:
            base_cmd.append(f"{username}@{hostname}")
        else:
            base_cmd.append(host_label)
    else:
        # SSH/SFTP format: ssh [options] Host
        # Follow the `ssh Host` pattern - use host_label which SSH will resolve from config
        # Only use user@hostname format if we have explicit values and they differ from config
        resolved_user = _get_ssh_config_value(effective_config, 'user')
        resolved_hostname = _get_ssh_config_value(effective_config, 'hostname')
        
        # If SSH config has user/hostname, prefer using host_label (SSH will resolve it)
        # Otherwise, use explicit user@hostname if provided
        if resolved_user or resolved_hostname:
            # SSH config has user/hostname - use host_label pattern
            base_cmd.append(host_label)
        elif username and hostname:
            # No SSH config user/hostname - use explicit values
            base_cmd.append(f"{username}@{hostname}")
        else:
            # Fallback to host_label
            base_cmd.append(host_label)
    
    # Add remote command after host (if specified)
    if ctx.remote_command:
        # Ensure shell remains active
        final_remote_cmd = ctx.remote_command if 'exec $SHELL' in ctx.remote_command else f"{ctx.remote_command} ; exec $SHELL -l"
        base_cmd.append(final_remote_cmd)
        logger.debug(f"Remote command: {final_remote_cmd}")
    
    # Force TTY for interactive password prompts if needed
    if use_password_auth and not has_stored_password:
        if '-t' not in base_cmd and '-tt' not in base_cmd:
            base_cmd.append('-t')
            logger.debug("Forcing TTY for interactive password prompt")
    
    # 6. Determine if we need sshpass for automatic password authentication
    if ctx.port_forwarding_rules:
        def _format_forward_host(host: str) -> str:
            host = (host or '').strip()
            if not host:
                return host
            if ':' in host and not (host.startswith('[') and host.endswith(']')):
                return f"[{host}]"
            return host
        
        for rule in ctx.port_forwarding_rules:
            if not rule.get('enabled', True):
                continue
            
            rule_type = rule.get('type')
            listen_addr = (rule.get('listen_addr') or 'localhost').strip()
            listen_port = rule.get('listen_port')
            listen_addr_cli = _format_forward_host(listen_addr) or listen_addr or 'localhost'
            
            if rule_type == 'dynamic' and listen_port:
                base_cmd.extend(['-D', f"{listen_addr_cli}:{listen_port}"])
                logger.debug(f"Added dynamic port forwarding: {listen_addr_cli}:{listen_port}")
            elif rule_type == 'local' and listen_port and 'remote_host' in rule and 'remote_port' in rule:
                remote_host = rule.get('remote_host', 'localhost')
                remote_port = rule.get('remote_port')
                remote_host_cli = _format_forward_host(remote_host) or remote_host or 'localhost'
                base_cmd.extend(['-L', f"{listen_addr_cli}:{listen_port}:{remote_host_cli}:{remote_port}"])
                logger.debug(f"Added local port forwarding: {listen_addr_cli}:{listen_port} -> {remote_host_cli}:{remote_port}")
            elif rule_type == 'remote' and listen_port:
                local_host = rule.get('local_host') or rule.get('remote_host', 'localhost')
                local_port = rule.get('local_port') or rule.get('remote_port')
                if local_port:
                    local_host_cli = _format_forward_host(local_host) or local_host or 'localhost'
                    base_cmd.extend(['-R', f"{listen_addr_cli}:{listen_port}:{local_host_cli}:{local_port}"])
                    logger.debug(f"Added remote port forwarding: {listen_addr_cli}:{listen_port} -> {local_host_cli}:{local_port}")
    
    # Add X11 forwarding if enabled in connection
    if getattr(connection, 'x11_forwarding', False):
        base_cmd.append('-X')
        logger.debug("X11 forwarding enabled")
    
    # Add remote command TTY allocation if specified (before host)
    if ctx.remote_command:
        base_cmd.extend(['-t', '-t'])  # Force TTY allocation
    
    # Add local command if specified
    if ctx.local_command:
        base_cmd.extend(['-o', 'PermitLocalCommand=yes'])
        base_cmd.extend(['-o', f'LocalCommand={ctx.local_command}'])
        logger.debug(f"Local command enabled: {ctx.local_command}")
    
    # Add NumberOfPasswordPrompts
    base_cmd.extend(['-o', 'NumberOfPasswordPrompts=1'])
    
    # Add PreferredAuthentications based on auth method
    if password_auth_selected:
        # Force password authentication when user chose password auth
        base_cmd.extend(['-o', 'PreferredAuthentications=password'])
        if getattr(connection, 'pubkey_auth_no', False):
            base_cmd.extend(['-o', 'PubkeyAuthentication=no'])
    elif has_stored_password:
        # Allow all standard authentication methods if password exists
        base_cmd.extend(['-o', 'PreferredAuthentications=gssapi-with-mic,hostbased,publickey,keyboard-interactive,password'])
    
    # Add ExitOnForwardFailure
    base_cmd.extend(['-o', 'ExitOnForwardFailure=yes'])
    
    # 8. Support appending extra SSH options as required
    if ctx.extra_ssh_config:
        # Parse extra SSH config options (from advanced tab)
        for line in ctx.extra_ssh_config.split('\n'):
            line = line.strip()
            if line and not line.startswith('#'):  # Skip empty lines and comments
                # Split on first space to separate option and value
                parts = line.split(' ', 1)
                if len(parts) == 2:
                    option, value = parts
                    base_cmd.extend(['-o', f"{option}={value}"])
                    logger.debug(f"Added extra SSH option: {option}={value}")
                elif len(parts) == 1:
                    # Option without value (e.g., "Compression yes" becomes "Compression=yes")
                    option = parts[0]
                    base_cmd.extend(['-o', f"{option}=yes"])
                    logger.debug(f"Added extra SSH option: {option}=yes")
    
    # Add known hosts file if specified
    if ctx.known_hosts_path and os.path.exists(ctx.known_hosts_path):
        base_cmd.extend(['-o', f'UserKnownHostsFile={ctx.known_hosts_path}'])
        logger.debug(f"Using custom known hosts file: {ctx.known_hosts_path}")
    
    # Add extra arguments (before host)
    if extra_args:
        base_cmd.extend(extra_args)
    
    # 6. Determine if we need sshpass for automatic password authentication
    use_sshpass = False
    password_for_sshpass = None
    
    if use_password_auth and stored_password:
        use_sshpass = True
        password_for_sshpass = stored_password
        logger.debug("Using sshpass for automatic password authentication")
    
    # Log final environment state
    logger.info(f"build_ssh_connection returning: SSH_ASKPASS={env.get('SSH_ASKPASS', 'NOT_SET')}, SSH_ASKPASS_REQUIRE={env.get('SSH_ASKPASS_REQUIRE', 'NOT_SET')}, use_askpass={use_askpass}, has_saved_passphrase={has_saved_passphrase}")
    
    return SSHConnectionCommand(
        command=base_cmd,
        env=env,
        use_sshpass=use_sshpass,
        password=password_for_sshpass,
        use_askpass=use_askpass
    )


def execute_ssh_connection(
    cmd: SSHConnectionCommand,
    **subprocess_kwargs
) -> subprocess.CompletedProcess:
    """
    Execute an SSH connection command.
    
    Handles sshpass wrapping and environment setup automatically.
    """
    if cmd.use_sshpass and cmd.password:
        # Use sshpass for password authentication
        host = None
        user = None
        port = 22
        
        # Extract host, user, port from command
        for i, arg in enumerate(cmd.command):
            if '@' in arg and not arg.startswith('-'):
                parts = arg.split('@')
                if len(parts) == 2:
                    user = parts[0]
                    host_part = parts[1]
                    if ':' in host_part:
                        host, port_str = host_part.rsplit(':', 1)
                        try:
                            port = int(port_str)
                        except ValueError:
                            pass
                    else:
                        host = host_part
                break
        
        if not host:
            # Fallback: try to find host in command
            for arg in cmd.command:
                if not arg.startswith('-') and '@' not in arg and arg not in ['ssh', 'scp', 'sftp']:
                    host = arg
                    break
        
        if host:
            # Extract SSH options
            ssh_opts = []
            i = 0
            while i < len(cmd.command):
                if cmd.command[i] == '-o' and i + 1 < len(cmd.command):
                    ssh_opts.append('-o')
                    ssh_opts.append(cmd.command[i + 1])
                    i += 2
                elif cmd.command[i].startswith('-'):
                    ssh_opts.append(cmd.command[i])
                    i += 1
                else:
                    i += 1
            
            # Get extra args (command to run, etc.)
            extra_args = []
            found_host = False
            for arg in cmd.command:
                if '@' in arg or (not arg.startswith('-') and arg not in ['ssh', 'scp', 'sftp'] and not found_host):
                    found_host = True
                    continue
                if found_host:
                    extra_args.append(arg)
            
            if cmd.command[0] == 'scp':
                # Use SCP-specific password execution
                sources = [a for a in extra_args if not a.endswith(':')]
                destination = next((a for a in extra_args if a.endswith(':')), '')
                return run_scp_with_password(
                    host, user or '', cmd.password,
                    sources, destination,
                    port=port,
                    extra_ssh_opts=ssh_opts,
                    inherit_env=cmd.env
                )
            else:
                # Use SSH password execution
                return run_ssh_with_password(
                    host, user or '', cmd.password,
                    port=port,
                    argv_tail=extra_args,
                    extra_ssh_opts=ssh_opts,
                    inherit_env=cmd.env,
                    use_publickey=not getattr(cmd, 'force_password_only', False)
                )
    
    # Default: execute command directly
    return subprocess.run(
        cmd.command,
        env=cmd.env,
        **subprocess_kwargs
    )

