#!/usr/bin/env python3
"""
SSH Cryptography Configuration Generator

Generates configuration file /etc/ssh/sshd_config.d/91-Cryptography.conf
based on available SSH algorithms and user preferences.
Supports interactive selection, security equivalent verification,
and DH moduli validation based on OpenBSD sshd_config(5) specification.
"""

import subprocess
import sys
import os
import re
from typing import Dict, List, Tuple, Optional
from pathlib import Path
from datetime import datetime


# Mapping of ssh -Q parameters to sshd_config variables
ALGORITHM_MAPPING = {
    'cipher': 'Ciphers',
    'cipher-auth': 'AuthenticatedEncryption',
    'mac': 'MACs',
    'kex': 'KexAlgorithms',
    'kex-gss': 'GssKexAlgorithms',
    'key': 'HostKeys',
    'key-ca-sign': 'CASignatureAlgorithms',
    'key-cert': 'CertificateAlgorithms',
    'key-plain': 'PubkeyAcceptedAlgorithms',
    'key-sig': 'HostKeyAlgorithms',
    'protocol-version': 'Protocol',
    'sig': 'SignatureAlgorithms',
}

# RSA key size security mapping (NIST SP 800-56Ar3)
RSA_KEY_SECURITY_MAP = {
    1024: 80,      # [DEPRECATED]
    2048: 112,     # [DEPRECATED]
    3072: 128,     # [TRANSITIONAL] (OpenSSH default)
    4096: 140,     # [TRANSITIONAL]
    6144: 180,     # Enhanced
    8192: 200,     # [CURRENT]
    15360: 256,    # [CURRENT] maximum
}

# Map security bits target to minimum RSA size (always minimum 3072)
SECURITY_BITS_TO_RSA_SIZE = {
    112: 3072,     # Minimum per policy
    128: 3072,     # OpenSSH default
    144: 4096,     # Enhanced security
    160: 6144,     # Strong security
    174: 6144,     # Strong security (180b actual)
    192: 8192,     # High security
    224: 15360,    # Highest security
    256: 15360,    # Maximum security
}

# Security equivalents per NIST SP 800-56Ar3, FIPS 140-2, and OpenBSD documentation
SECURITY_EQUIVALENTS = {
    # === SYMMETRIC CIPHERS (AES) ===
    '3des-cbc': '[DEPRECATED] 3DES-56x3 (112b)',
    'aes128-cbc': 'AES-128 (128b)',
    'aes128-ctr': 'AES-128 (128b)',
    'aes192-cbc': 'AES-192 (192b)',
    'aes192-ctr': 'AES-192 (192b)',
    'aes256-cbc': 'AES-256 (256b)',
    'aes256-ctr': 'AES-256 (256b)',
    'aes128-gcm@openssh.com': 'AES-128-GCM (128b)',
    'aes256-gcm@openssh.com': 'AES-256-GCM (256b)',
    'chacha20-poly1305@openssh.com': 'ChaCha20-Poly1305 (256b)',
    
    # === AUTHENTICATED ENCRYPTION ===
    
    # === DIFFIE-HELLMAN GROUPS (RFC 2409, RFC 3526) ===
    'diffie-hellman-group1-sha1': '[DEPRECATED] DH-768-bit (70b) + SHA1',
    'diffie-hellman-group1-sha256': '[DEPRECATED] DH-768-bit (70b)',
    'diffie-hellman-group14-sha1': '[DEPRECATED] DH-2048-bit (112b) + SHA1',
    'diffie-hellman-group14-sha256': 'DH-2048-bit (112b)',
    'diffie-hellman-group14-sha512': 'DH-2048-bit (112b)',
    'diffie-hellman-group15-sha512': 'DH-3072-bit (128b)',
    'diffie-hellman-group16-sha512': 'DH-4096-bit (140b)',
    'diffie-hellman-group17-sha512': 'DH-6144-bit (180b)',
    'diffie-hellman-group18-sha512': 'DH-8192-bit (200b)',
    'diffie-hellman-group-exchange-sha1': '[DEPRECATED] DH-GEX + SHA1',
    'diffie-hellman-group-exchange-sha256': 'DH-GEX variable (80-1024b)',
    
    # === ELLIPTIC CURVES (RFC 5903, RFC 8032) ===
    'ecdh-sha2-nistp256': 'NIST P-256 (128b)',
    'ecdh-sha2-nistp384': 'NIST P-384 (192b)',
    'ecdh-sha2-nistp521': 'NIST P-521 (256b)',
    'curve25519-sha256': 'Curve25519 (125b)',
    'curve25519-sha256@libssh.org': 'Curve25519 (125b)',
    'curve448-sha512': 'Curve448 (224b)',
    'mlkem768x25519-sha256': 'ML-KEM-768 + Curve25519 (256b)',
    'sntrup761x25519-sha512@openssh.com': 'sntrup761 + Curve25519 (256b)',
    'sntrup761x25519-sha512': 'sntrup761 + Curve25519 (256b)',
    
    # === MESSAGE AUTHENTICATION CODES ===
    'hmac-sha1': '[DEPRECATED] SHA1 (80b)',
    'hmac-sha1-96': '[DEPRECATED] SHA1-96 (80b)',
    'hmac-sha2-256': 'SHA2-256 (128b)',
    'hmac-sha2-512': 'SHA2-512 (256b)',
    'hmac-sha2-256-etm@openssh.com': 'SHA2-256-ETM (128b)',
    'hmac-sha2-512-etm@openssh.com': 'SHA2-512-ETM (256b)',
    'hmac-md5': '[DEPRECATED] MD5 (64b)',
    'hmac-md5-96': '[DEPRECATED] MD5-96 (64b)',
    'hmac-md5-etm@openssh.com': '[DEPRECATED] MD5-ETM (64b)',
    'hmac-md5-96-etm@openssh.com': '[DEPRECATED] MD5-96-ETM (64b)',
    'hmac-sha1-etm@openssh.com': '[DEPRECATED] SHA1-ETM (80b)',
    'hmac-sha1-96-etm@openssh.com': '[DEPRECATED] SHA1-96-ETM (80b)',
    'umac-64@openssh.com': 'UMAC-64 (64b)',
    'umac-128@openssh.com': 'UMAC-128 (128b)',
    'umac-64-etm@openssh.com': 'UMAC-64-ETM (64b)',
    'umac-128-etm@openssh.com': 'UMAC-128-ETM (128b)',
    
    # === DIGITAL SIGNATURES (Host Keys) ===
    'ssh-dss': '[DEPRECATED] DSA/DSS (80b)',
    'ssh-dss-cert-v01@openssh.com': '[DEPRECATED] DSA/DSS-CERT (80b)',
    'ssh-rsa': '[DEPRECATED] RSA-SHA1 (variable: security determined by RequiredRSASize)',
    'ssh-rsa-cert-v01@openssh.com': '[DEPRECATED] RSA-SHA1-CERT (variable: security determined by RequiredRSASize)',
    'rsa-sha2-256': 'RSA-SHA2-256 (variable: security determined by RequiredRSASize) - SHA2',
    'rsa-sha2-512': 'RSA-SHA2-512 (variable: security determined by RequiredRSASize) - SHA2',
    'rsa-sha2-256-cert-v01@openssh.com': 'RSA-SHA2-256-CERT (variable: security determined by RequiredRSASize) - SHA2',
    'rsa-sha2-512-cert-v01@openssh.com': 'RSA-SHA2-512-CERT (variable: security determined by RequiredRSASize) - SHA2',
    'ssh-ed25519': 'Ed25519 (125b)',
    'ssh-ed25519-cert-v01@openssh.com': 'Ed25519-CERT (125b)',
    'sk-ssh-ed25519@openssh.com': 'SK-Ed25519 (125b)',
    'sk-ssh-ed25519-cert-v01@openssh.com': 'SK-Ed25519-CERT (125b)',
    'ecdsa-sha2-nistp256': 'ECDSA-P256 (128b)',
    'ecdsa-sha2-nistp256-cert-v01@openssh.com': 'ECDSA-P256-CERT (128b)',
    'ecdsa-sha2-nistp384': 'ECDSA-P384 (192b)',
    'ecdsa-sha2-nistp384-cert-v01@openssh.com': 'ECDSA-P384-CERT (192b)',
    'ecdsa-sha2-nistp521': 'ECDSA-P521 (256b)',
    'ecdsa-sha2-nistp521-cert-v01@openssh.com': 'ECDSA-P521-CERT (256b)',
    'sk-ecdsa-sha2-nistp256@openssh.com': 'SK-ECDSA-P256 (128b)',
    'sk-ecdsa-sha2-nistp256-cert-v01@openssh.com': 'SK-ECDSA-P256-CERT (128b)',
    'webauthn-sk-ecdsa-sha2-nistp256@openssh.com': 'WebAuthn-SK-ECDSA-P256 (128b)',
    'webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com': 'WebAuthn-SK-ECDSA-P256-CERT (128b)',
    
    # === GSS KEY EXCHANGE (Kerberos/GSSAPI) ===
    'gss-gex-sha1-': '[DEPRECATED] GSS-GEX + SHA1 (80b)',
    'gss-group1-sha1-': '[DEPRECATED] GSS-DH-768 + SHA1 (70b)',
    'gss-group14-sha1-': '[DEPRECATED] GSS-DH-2048 + SHA1 (112b)',
    'gss-group14-sha256-': 'GSS-DH-2048 (112b)',
    'gss-group16-sha512-': 'GSS-DH-4096 (140b)',
    'gss-nistp256-sha256-': 'GSS-NIST-P256 (128b)',
    'gss-curve25519-sha256-': 'GSS-Curve25519 (125b)',
    
    # === PROTOCOL ===
    '2': 'SSH Protocol Version 2 (current)',
}

# Description of clauses with supported algorithms from OpenBSD sshd_config(5)
CLAUSE_DESCRIPTIONS = {
    'Ciphers': (
        'Symmetric ciphers for SSH connection encryption. Default: chacha20-poly1305@openssh.com, '
        'aes128-gcm@openssh.com, aes256-gcm@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr. '
        'Supported: 3des-cbc, aes128-cbc, aes192-cbc, aes256-cbc, aes128-ctr, aes192-ctr, aes256-ctr, '
        'aes128-gcm@openssh.com, aes256-gcm@openssh.com, chacha20-poly1305@openssh.com. '
        'AES-256 and ChaCha20 are recommended.'
    ),
    'AuthenticatedEncryption': (
        'Authenticated encryption ciphers combining encryption and integrity verification. '
        'OpenSSH extension. Supported: aes128-gcm@openssh.com, aes256-gcm@openssh.com, '
        'chacha20-poly1305@openssh.com.'
    ),
    'MACs': (
        'Message Authentication Codes for data integrity verification. Default: '
        'umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, '
        'hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, '
        'umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1. '
        'Algorithms with "-etm" (encrypt-then-MAC) are safer. SHA2 variants are secure; '
        'SHA1 and MD5 are compromised. Supported: hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96, '
        'hmac-sha2-256, hmac-sha2-512, umac-64@openssh.com, umac-128@openssh.com, and *-etm variants.'
    ),
    'KexAlgorithms': (
        'Key Exchange algorithms determining how parties negotiate a shared secret. '
        'Default: mlkem768x25519-sha256, sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, '
        'curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521. '
        'Supported: curve25519-sha256, curve25519-sha256@libssh.org, diffie-hellman-group1-sha1, '
        'diffie-hellman-group14-sha1, diffie-hellman-group14-sha256, diffie-hellman-group16-sha512, '
        'diffie-hellman-group18-sha512, diffie-hellman-group-exchange-sha1, diffie-hellman-group-exchange-sha256, '
        'ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, mlkem768x25519-sha256, '
        'sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com. '
        'DH groups with SHA1 are deprecated.'
    ),
    'GssKexAlgorithms': (
        'GSS (Generic Security Service) variants of key exchange for Kerberos and GSSAPI authentication. '
        'These are GSS versions of standard algorithms.'
    ),
    'HostKeys': (
        'Host key types used by the server for identification. Default: '
        '/etc/ssh/ssh_host_ecdsa_key, /etc/ssh/ssh_host_ed25519_key, /etc/ssh/ssh_host_rsa_key. '
        'Ed25519 and ECDSA variants are recommended. '
        'RSA: supports variable key length (1024-4096+ bits), with minimum size enforced by RequiredRSASize directive. '
        'Common deployments: 2048-4096 bits. Modern deployments recommend 3072-bit or higher RSA keys.'
    ),
    'CASignatureAlgorithms': (
        'Algorithms allowed for signing certificates by certificate authorities (CAs). '
        'Default: ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, '
        'sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com, rsa-sha2-512, rsa-sha2-256. '
        'Certificates signed with other algorithms will not be accepted.'
    ),
    'CertificateAlgorithms': (
        'Host and user certificates in OpenSSH format. Specifies which certificate types are acceptable. '
        'Derived from HostKeyAlgorithms variants.'
    ),
    'PubkeyAcceptedAlgorithms': (
        'Public key signature algorithms accepted for user authentication. Default: '
        'ssh-ed25519-cert-v01@openssh.com, ecdsa-sha2-nistp256-cert-v01@openssh.com, '
        'ecdsa-sha2-nistp384-cert-v01@openssh.com, ecdsa-sha2-nistp521-cert-v01@openssh.com, '
        'sk-ssh-ed25519-cert-v01@openssh.com, sk-ecdsa-sha2-nistp256-cert-v01@openssh.com, '
        'webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com, '
        'rsa-sha2-512-cert-v01@openssh.com, rsa-sha2-256-cert-v01@openssh.com, '
        'ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, '
        'sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com, '
        'webauthn-sk-ecdsa-sha2-nistp256@openssh.com, rsa-sha2-512, rsa-sha2-256. '
        'Older SSH clients may require RSA and ECDSA variants.'
    ),
    'HostKeyAlgorithms': (
        'Host key algorithms advertised by the server. Default: '
        'ssh-ed25519-cert-v01@openssh.com, ecdsa-sha2-nistp256-cert-v01@openssh.com, '
        'ecdsa-sha2-nistp384-cert-v01@openssh.com, ecdsa-sha2-nistp521-cert-v01@openssh.com, '
        'sk-ssh-ed25519-cert-v01@openssh.com, sk-ecdsa-sha2-nistp256-cert-v01@openssh.com, '
        'webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com, '
        'rsa-sha2-512-cert-v01@openssh.com, rsa-sha2-256-cert-v01@openssh.com, '
        'ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, '
        'sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com, '
        'webauthn-sk-ecdsa-sha2-nistp256@openssh.com, rsa-sha2-512, rsa-sha2-256. '
        'Must correspond to available private host keys.'
    ),
    'SignatureAlgorithms': (
        'Algorithms for digital signatures during authentication. Default: ssh-ed25519, '
        'ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, rsa-sha2-512, rsa-sha2-256. '
        'When RSA is required, prefer rsa-sha2-512 (RSA-PSS). Must match available public keys.'
    ),
    'Protocol': (
        'SSH protocol version. Only SSH Protocol Version 2 is secure and supported by modern OpenSSH. '
        'SSH Protocol Version 1 is obsolete and dangerous.'
    ),
}


def extract_security_value(equiv: str) -> Optional[int]:
    """Extract numeric security value from equivalence string."""
    match = re.search(r'\((\d+)b\)', equiv)
    if match:
        return int(match.group(1))
    return None


def get_algorithm_security_bits(algo: str) -> Optional[int]:
    """
    Extract security bits equivalent from algorithm description.
    
    Args:
        algo: Algorithm name (e.g., 'aes128-cbc', 'rsa-sha2-256')
        
    Returns:
        int: Security bits, or None if cannot determine
        Note: Returns None for rsa-sha2-* (security determined by RSA key size, not fixed)
        Note: Ed25519 and Curve25519 (125b) are treated as 128b for filtering purposes
    """
    # Special case: rsa-sha2-* have variable security based on RSA key size
    # Security is determined by RequiredRSASize, not by the algorithm itself
    if algo in ('rsa-sha2-256', 'rsa-sha2-512', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'):
        return None  # Variable security, determined by RSA key size
    
    # Special case: Ed25519 and Curve25519 (125b) are treated as 128b for filtering
    # They are displayed as 125b but logically considered 128b for security minimum
    if algo in ('ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 
                'sk-ssh-ed25519-cert-v01@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org',
                'gss-curve25519-sha256-'):
        return 128  # Treat 125b as 128b for filtering purposes
    
    equiv = SECURITY_EQUIVALENTS.get(algo, "")
    if not equiv:
        return None
    
    # Try standard pattern: (XXXb)
    bits = extract_security_value(equiv)
    if bits is not None:
        return bits
    
    # For algorithms with other ranges, return minimum
    if 'min' in equiv and 'max' in equiv:
        matches = re.findall(r'(\d+)b', equiv)
        if matches:
            return int(matches[0])  # Return minimum
    
    return None


def is_algorithm_deprecated(algo: str) -> bool:
    """
    Check if algorithm is marked as deprecated or obsolete.
    
    Args:
        algo: Algorithm name
        
    Returns:
        bool: True if deprecated or obsolete, False otherwise
    """
    equiv = SECURITY_EQUIVALENTS.get(algo, "")
    return '[DEPRECATED]' in equiv or '[BROKEN]' in equiv or '[OBSOLETE]' in equiv


def should_algorithm_be_selected(algo: str, min_security_bits: int) -> bool:
    """
    Determine if algorithm meets security requirements and is not deprecated.
    
    Args:
        algo: Algorithm name
        min_security_bits: Minimum security bits required (from RSA size)
        
    Returns:
        bool: True if algorithm should be pre-selected, False otherwise
    """
    # Reject deprecated/obsolete algorithms
    if is_algorithm_deprecated(algo):
        return False
    
    # Special case: rsa-sha2-* algorithms inherit security from RSA key size
    # They are always available since security is determined by user's RSA choice
    if algo in ('rsa-sha2-256', 'rsa-sha2-512', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'):
        # These algorithms are always selected - their security matches the RSA key size
        return True
    
    # Get algorithm security bits (125b treated as 128b for Curve25519/Ed25519)
    algo_bits = get_algorithm_security_bits(algo)
    if algo_bits is None:
        # If we can't determine security, assume it meets minimum
        return True
    
    # Check if meets minimum security requirement
    return algo_bits >= min_security_bits


def get_security_classification(security_bits: int) -> str:
    """
    Classify security equivalent by bits per NIST SP 800-56Ar3 recommendations.
    
    Args:
        security_bits: Security bits (e.g., 112, 128, 256)
        
    Returns:
        str: Classification label "[DEPRECATED]", "[TRANSITIONAL]", or "[CURRENT]"
    """
    if security_bits < 128:
        return '[DEPRECATED]'
    elif security_bits < 256:
        return '[TRANSITIONAL]'
    else:
        return '[CURRENT]'


def get_dh_security_equivalent_by_size(bit_size: int) -> str:
    """
    Map DH group bit size to security equivalent.
    Based on OpenBSD moduli(5) documentation.
    
    Args:
        bit_size: DH group size in bits (from moduli file field 4)
        
    Returns:
        str: Security equivalent descriptor
    """
    # Map bit sizes to security equivalents per NIST SP 800-56Ar3
    if bit_size <= 512:
        return '[BROKEN] DH (≤512b) - COMPROMISED!'
    elif bit_size == 768:
        return '[DEPRECATED] DH-768-bit (70b)'
    elif bit_size == 1024:
        return '[DEPRECATED] DH-1024-bit (80b)'
    elif bit_size == 1536:
        return 'DH-1536-bit (95b)'
    elif bit_size == 2048:
        return 'DH-2048-bit (112b)'
    elif bit_size == 3072:
        return 'DH-3072-bit (128b)'
    elif bit_size == 4096:
        return 'DH-4096-bit (140b)'
    elif bit_size == 6144:
        return 'DH-6144-bit (180b)'
    elif bit_size == 8192:
        return 'DH-8192-bit (200b)'
    else:
        # Estimate for non-standard sizes
        estimated_bits = max(70, bit_size // 15)
        return f'DH-{bit_size}-bit ({estimated_bits}b)'


def get_minimum_rsa_security_from_algorithms(selected_algorithms: Dict[str, List[str]]) -> Optional[int]:
    """
    Find minimum security equivalent from all selected RSA algorithms.
    
    Args:
        selected_algorithms: Dictionary of algorithm type → list of selected algorithms
        
    Returns:
        int: Minimum security bits found, or None if no RSA algorithms selected
    """
    rsa_algorithms = ['ssh-rsa', 'ssh-rsa-cert-v01@openssh.com', 'rsa-sha2-256', 
                      'rsa-sha2-512', 'rsa-sha2-256-cert-v01@openssh.com', 
                      'rsa-sha2-512-cert-v01@openssh.com']
    
    min_security = None
    
    # Search all algorithm categories
    for algo_list in selected_algorithms.values():
        for algo in algo_list:
            if algo in rsa_algorithms:
                # For RSA algorithms, extract security from the mapping
                # RSA-SHA2 algorithms: use minimum from the format "depends on key: 2048=112b, 3072=128b, 4096=140b"
                # ssh-rsa: use default 3072=128b
                if algo.startswith('ssh-rsa'):
                    # ssh-rsa and ssh-rsa-cert use 3072-bit default = 128b
                    bits = 128
                elif algo.startswith('rsa-sha2'):
                    # rsa-sha2-* algorithms use minimum from their range
                    # Find all numeric matches in format like "2048=112b, 3072=128b"
                    equiv = SECURITY_EQUIVALENTS.get(algo, "")
                    # Extract all (size=bits) pairs
                    matches = re.findall(r'(\d+)b(?:,|$)', equiv)
                    if matches:
                        bits = min(int(b) for b in matches)
                    else:
                        bits = 112  # Fallback to minimum
                else:
                    equiv = SECURITY_EQUIVALENTS.get(algo, "")
                    bits = extract_security_value(equiv)
                    if bits is None:
                        bits = 112  # Fallback for unknown formats
                
                if bits is not None:
                    if min_security is None or bits < min_security:
                        min_security = bits
    
    return min_security


def get_required_rsa_size(min_security_bits: Optional[int], override_bits: Optional[int] = None) -> int:
    """
    Calculate RequiredRSASize based on minimum security equivalent.
    
    Args:
        min_security_bits: Minimum security bits from selected RSA algorithms
        override_bits: Optional user override for security bits target
        
    Returns:
        int: Required RSA size in bits
    """
    target_bits = override_bits if override_bits else min_security_bits
    
    if target_bits is None:
        # No RSA algorithms selected, return OpenSSH default
        return 3072
    
    # Find matching RSA size from mapping
    rsa_size = SECURITY_BITS_TO_RSA_SIZE.get(target_bits)
    
    if rsa_size is None:
        # Find closest match
        available = sorted(SECURITY_BITS_TO_RSA_SIZE.keys())
        for i, bits in enumerate(available):
            if target_bits <= bits:
                rsa_size = SECURITY_BITS_TO_RSA_SIZE[bits]
                break
        
        if rsa_size is None:
            # Use maximum
            rsa_size = SECURITY_BITS_TO_RSA_SIZE[available[-1]]
    
    # Enforce minimum 3072
    return max(3072, rsa_size)


class SSHCryptoConfigGenerator:
    """SSH Cryptography Configuration Generator with security validation."""
    
    def __init__(self):
        """Initialize the generator."""
        self.algorithms: Dict[str, List[str]] = {}
        self.selected_algorithms: Dict[str, List[str]] = {}
        self.required_rsa_size: int = 3072  # Default
        self.rsa_security_override: Optional[int] = None  # User override
    
    def fetch_ssh_algorithms(self) -> bool:
        """
        Fetch available SSH algorithms via ssh -Q command.
        
        Returns:
            bool: True if successful, False otherwise
        """
        bash_command = (
            'for QUE in cipher cipher-auth mac kex kex-gss key key-ca-sign '
            'key-cert key-plain key-sig protocol-version sig; do '
            'echo "# SSH Query $QUE" && ssh -Q $QUE 2>/dev/null; done'
        )
        
        try:
            result = subprocess.run(
                bash_command,
                shell=True,
                capture_output=True,
                text=True,
                timeout=30
            )
            
            if result.returncode != 0:
                print("[ERROR] Failed to execute ssh command.", file=sys.stderr)
                if result.stderr:
                    print(f"Details: {result.stderr}", file=sys.stderr)
                return False
            
            return self._parse_output(result.stdout)
            
        except subprocess.TimeoutExpired:
            print("[ERROR] Command timeout.", file=sys.stderr)
            return False
        except Exception as e:
            print(f"[ERROR] Command execution error: {e}", file=sys.stderr)
            return False
    
    def _parse_output(self, output: str) -> bool:
        """
        Parse ssh -Q command output.
        
        Args:
            output: Command output
            
        Returns:
            bool: True if parsing successful
        """
        current_type = None
        algorithms = {}
        
        for line in output.strip().split('\n'):
            line = line.strip()
            
            if not line:
                continue
            
            if line.startswith('# SSH Query'):
                parts = line.split()
                if len(parts) >= 4:
                    current_type = parts[3]
                    if current_type not in algorithms:
                        algorithms[current_type] = []
            elif current_type and not line.startswith('#'):
                algorithms[current_type].append(line)
        
        if not algorithms:
            print("[ERROR] Failed to parse output.", file=sys.stderr)
            return False
        
        self.algorithms = algorithms
        return True
    
    # NOTE: check_security_equivalents removed - no longer needed
    # Mixed security equivalents are normal and handled by pre-filtering
    # Warnings about mixed security were removed for cleaner output
    
    def display_algorithms(self) -> None:
        """Display available algorithms with security equivalents (removed - no longer needed)."""
        # Algorithm display removed - not needed for clean interactive selection
        pass
    
    def _select_protocol_first(self) -> None:
        """
        Select SSH Protocol version (STEP 1).
        Called before RSA security configuration.
        """
        print("\n" + "="*70)
        print("STEP 1: SELECT SSH PROTOCOL VERSION")
        print("="*70)
        
        if 'protocol-version' not in self.algorithms:
            print("[ERROR] Protocol version not available.")
            return
        
        protocols = self.algorithms['protocol-version']
        selected = self._interactive_algorithm_selector('Protocol', protocols, min_security=0)
        self.selected_algorithms['protocol-version'] = selected
    
    def interactive_selection(self) -> bool:
        """
        Interactive algorithm selection with security checking.
        Applies RSA security minimum to algorithm filtering.
        Skips protocol-version (already selected in _select_protocol_first).
        
        Returns:
            bool: True if selection successful
        """
        # NOTE: Do NOT reset selected_algorithms here - protocol-version is already selected
        # Initialize only missing keys
        if 'selected_algorithms' not in dir(self) or not isinstance(self.selected_algorithms, dict):
            self.selected_algorithms = {}
        
        # Get minimum security from RSA configuration
        min_security = self.rsa_security_override if self.rsa_security_override else 112
        
        # Process all algorithms except protocol-version (already selected)
        for algo_type in sorted(self.algorithms.keys()):
            if algo_type == 'protocol-version':
                continue  # Already selected in STEP 1
            
            algos = self.algorithms[algo_type]
            sshd_var = ALGORITHM_MAPPING.get(algo_type, algo_type)
            
            # Interactive selection for this category with security filtering
            selected = self._interactive_algorithm_selector(sshd_var, algos, min_security)
            self.selected_algorithms[algo_type] = selected
        
        return True
    
    def _configure_rsa_security_first(self) -> None:
        """
        Configure RequiredRSASize FIRST, before algorithm selection.
        Offers all 8 security levels without filtering.
        """
        print("\n" + "="*70)
        print("RSA SECURITY CONFIGURATION (STEP 1)")
        print("="*70)
        print("[INFO] Configure minimum RSA key size requirement")
        print("[INFO] This setting will be applied to any RSA algorithms selected later\n")
        
        # Show all available security levels
        sorted_levels = sorted(SECURITY_BITS_TO_RSA_SIZE.items())
        print("Available RSA security levels (with RequiredRSASize):")
        for i, (bits, rsa_size) in enumerate(sorted_levels, 1):
            marker = "→" if i == 2 else " "  # Default to 128b (3072-bit)
            security_class = get_security_classification(bits)
            print(f"  {marker} {i}. {bits:3d}b security ({rsa_size:5d}-bit key) {security_class}")
        
        print("\nOptions:")
        print("  Enter number (1-8) to select security level")
        print("  Press Enter for default (option 2: 128b/3072-bit)")
        
        while True:
            choice = input("\nChoice [default=2]: ").strip()
            
            if choice == "":
                # Default: 128b (3072-bit)
                self.required_rsa_size = 3072
                self.rsa_security_override = 128
                print("[OK] RequiredRSASize set to: 3072-bit (security: 128b)")
                break
            
            try:
                idx = int(choice)
                if 1 <= idx <= len(sorted_levels):
                    security_bits = sorted_levels[idx - 1][0]
                    self.required_rsa_size = sorted_levels[idx - 1][1]
                    self.rsa_security_override = security_bits
                    print(f"[OK] RequiredRSASize set to: {self.required_rsa_size}-bit (security: {security_bits}b)")
                    break
                else:
                    print(f"[ERROR] Please enter number 1-{len(sorted_levels)}")
            except ValueError:
                print("[ERROR] Invalid input - please enter a number")
    
    def _configure_rsa_security(self) -> None:
        """
        Validate and inform about RequiredRSASize after algorithm selection.
        Shows information about detected minimum and enforced level.
        """
        # Find minimum RSA security from selected algorithms
        min_rsa_security = get_minimum_rsa_security_from_algorithms(self.selected_algorithms)
        
        if min_rsa_security is None:
            print("\n[INFO] No RSA algorithms selected - RequiredRSASize will not be generated")
            return
        
        # Check if user override meets detected minimum
        print("\n" + "="*70)
        print("RSA SECURITY VALIDATION")
        print("="*70)
        print(f"[INFO] Detected minimum RSA security from selected algorithms: {min_rsa_security}b")
        print(f"[INFO] Your selected RequiredRSASize: {self.required_rsa_size}-bit (security: {self.rsa_security_override}b)")
        
        if self.rsa_security_override >= min_rsa_security:
            print(f"[OK] RequiredRSASize setting is compatible with selected RSA algorithms")
        else:
            print(f"[WARNING] Your RequiredRSASize ({self.rsa_security_override}b) is weaker than detected minimum ({min_rsa_security}b)")
            print("[WARNING] The generated configuration will enforce the stronger minimum")
    
    
    def _interactive_algorithm_selector(self, var_name: str, algos: List[str], min_security: Optional[int] = None) -> List[str]:
        """
        Interactive selector with visual interface for algorithm selection.
        Filters algorithms by security level based on RSA configuration.
        
        Args:
            var_name: Configuration variable name (Ciphers, MACs, etc.)
            algos: List of available algorithms
            min_security: Minimum security bits required (from RSA config)
            
        Returns:
            List[str]: List of selected algorithms
        """
        # Determine minimum security from parameter or fallback
        if min_security is None:
            min_security = self.rsa_security_override if self.rsa_security_override else 112
        
        # Pre-select algorithms that meet security requirements and are not deprecated
        selected_indices = set()
        for i, algo in enumerate(algos):
            if should_algorithm_be_selected(algo, min_security):
                selected_indices.add(i)
        
        while True:
            print(f"\n{'='*100}")
            print(f"Selection for: {var_name} ({len(selected_indices)}/{len(algos)} selected)")
            print(f"{'='*100}")
            
            self._display_selection_list(algos, selected_indices, var_name, min_security)
            
            print("\nCommands:")
            print("  [numbers]  - select/deselect (1, 1,3,5, 1-5, etc.)")
            print("  a          - select all")
            print("  n          - deselect all")
            print("  i          - invert selection")
            print("  Enter      - confirm and continue")
            print("  q          - back")
            
            choice = input(f"\nChoice: ").strip().lower()
            
            if choice == '' or choice == 'ok':
                break
            elif choice == 'q':
                break
            elif choice == 'a':
                selected_indices = set(range(len(algos)))
                print(f"[OK] All selected ({len(selected_indices)}/{len(algos)})")
            elif choice == 'n':
                selected_indices = set()
                print(f"[OK] None selected")
            elif choice == 'i':
                all_indices = set(range(len(algos)))
                selected_indices = all_indices - selected_indices
                print(f"[OK] Selection inverted ({len(selected_indices)}/{len(algos)})")
            else:
                # Parse selection
                new_indices = self._parse_index_selection(choice, len(algos))
                if new_indices is not None:
                    selected_indices = new_indices
                    print(f"[OK] {len(selected_indices)}/{len(algos)} selected")
        
        # Return selected algorithms in original order
        return [algos[i] for i in sorted(selected_indices)]
    
    def _display_selection_list(self, algos: List[str], selected_indices: set, var_name: str = "", min_security: Optional[int] = None) -> None:
        """
        Display algorithm list with visual markers for selected items.
        Shows security equivalents with reasons why items are disabled.
        
        Args:
            algos: List of algorithms
            selected_indices: Set of selected algorithm indices
            var_name: Configuration variable name
            min_security: Minimum security bits required (from RSA config)
        """
        if min_security is None:
            min_security = 112  # Default fallback
        
        for i, algo in enumerate(algos):
            marker = "[X]" if i in selected_indices else "[ ]"
            sec_equiv = SECURITY_EQUIVALENTS.get(algo, "")
            
            # Determine why algorithm might be disabled
            disabled_reason = ""
            if i not in selected_indices:
                # If [DEPRECATED] is already in description, don't duplicate it
                if '[DEPRECATED]' not in sec_equiv and '[BROKEN]' not in sec_equiv:
                    # Only show reason if not already obvious from description
                    algo_bits = get_algorithm_security_bits(algo)
                    if algo_bits is not None and algo_bits < min_security:
                        disabled_reason = f" [DISABLED: Below {min_security}b minimum]"
                else:
                    # Already marked as deprecated/broken in description, just show [DISABLED]
                    disabled_reason = " [DISABLED]"
            
            if sec_equiv:
                print(f"{marker} {i+1:2d}. {algo:<50} {sec_equiv}{disabled_reason}")
            else:
                print(f"{marker} {i+1:2d}. {algo}{disabled_reason}")
    
    def _parse_index_selection(self, selection_str: str, max_index: int) -> Optional[set]:
        """
        Parse index selection from string (1, 1-5, 1,3,5, etc.).
        
        Args:
            selection_str: Selection string
            max_index: Maximum index (list size)
            
        Returns:
            set: Set of selected indices or None if error
        """
        selected = set()
        
        try:
            parts = selection_str.split(',')
            for part in parts:
                part = part.strip()
                if not part:
                    continue
                
                # Range check (1-5)
                if '-' in part:
                    range_parts = part.split('-')
                    if len(range_parts) == 2:
                        start = int(range_parts[0].strip())
                        end = int(range_parts[1].strip())
                        if start < 1 or end < 1 or start > max_index or end > max_index:
                            print(f"[ERROR] Numbers out of range (1-{max_index}): {part}")
                            return None
                        for i in range(start, end + 1):
                            selected.add(i - 1)
                    else:
                        print(f"[ERROR] Invalid range: {part}")
                        return None
                else:
                    # Single number
                    idx = int(part)
                    if idx < 1 or idx > max_index:
                        print(f"[ERROR] Number {idx} out of range (1-{max_index})")
                        return None
                    selected.add(idx - 1)
            
            if not selected:
                print("[ERROR] No algorithms selected")
                return None
            
            return selected
        
        except ValueError:
            print(f"[ERROR] Invalid input: '{selection_str}'")
            return None
    
    def backup_moduli_file(self) -> Tuple[bool, Optional[str]]:
        """
        Backup /etc/ssh/moduli file with timestamp.
        Falls back to current directory if permission denied.
        
        Returns:
            Tuple[bool, str]: (success, path_to_backup)
        """
        moduli_path = "/etc/ssh/moduli"
        
        if not Path(moduli_path).exists():
            print(f"[WARNING] {moduli_path} does not exist")
            return False, None
        
        timestamp = datetime.now().strftime("%Y%m%d")
        backup_path = f"{moduli_path}.{timestamp}"
        
        try:
            # Try to backup in system directory
            with open(moduli_path, 'r') as src:
                content = src.read()
            
            with open(backup_path, 'w') as dst:
                dst.write(content)
            
            print(f"[OK] Backed up: {backup_path}")
            return True, backup_path
        
        except PermissionError:
            # Fallback to current directory
            local_backup = f"moduli.{timestamp}"
            
            try:
                with open(moduli_path, 'r') as src:
                    content = src.read()
                
                with open(local_backup, 'w') as dst:
                    dst.write(content)
                
                print(f"[WARNING] System backup failed. Backed up to: {local_backup}")
                return True, local_backup
            
            except Exception as e:
                print(f"[ERROR] Failed to backup moduli: {e}", file=sys.stderr)
                return False, None
    
    def filter_moduli_by_security(self, selected_kex: List[str], selected_gss: List[str]) -> Optional[str]:
        """
        Filter /etc/ssh/moduli file to keep DH groups matching security level of selected algorithms.
        Evaluates based on security equivalents from moduli file field 4 (size in bits).
        Only considers non-broken/non-deprecated algorithms for security level calculation.
        
        Security classification: [DEPRECATED] <128b, [TRANSITIONAL] 128-192b, [CURRENT] >=256b
        
        Based on OpenBSD moduli(5) format:
        Fields: timestamp | type | tests | trials | size | generator | modulus
        
        Args:
            selected_kex: Selected KexAlgorithms
            selected_gss: Selected GssKexAlgorithms
            
        Returns:
            str: Filtered moduli content or None if error
        """
        moduli_path = "/etc/ssh/moduli"
        
        # Extract DH security equivalents from selected algorithms
        dh_security_levels = []
        for algo in selected_kex + selected_gss:
            equiv = SECURITY_EQUIVALENTS.get(algo, "")
            if 'DH-' in equiv or 'GSS-DH-' in equiv:
                dh_security_levels.append((algo, equiv))
        
        if not dh_security_levels:
            print("[INFO] No DH algorithms selected - moduli filtering skipped")
            return None
        
        # Determine minimum security level from HEALTHY algorithms only
        # (skip BROKEN/DEPRECATED algorithms for minimum calculation)
        min_security_bits = float('inf')
        for algo, equiv in dh_security_levels:
            # Skip broken/deprecated algorithms from minimum calculation
            if '[BROKEN]' in equiv or '[DEPRECATED]' in equiv:
                continue
            
            bits = extract_security_value(equiv)
            if bits is not None:
                if bits < min_security_bits:
                    min_security_bits = bits
        
        # If only broken/deprecated selected, use lowest non-zero security
        if min_security_bits == float('inf'):
            for algo, equiv in dh_security_levels:
                bits = extract_security_value(equiv)
                if bits is not None and bits > 0:
                    if bits < min_security_bits:
                        min_security_bits = bits
        
        if min_security_bits == float('inf'):
            min_security_bits = 112  # Default to DH-2048 security level
        
        min_classification = get_security_classification(int(min_security_bits))
        
        print(f"[INFO] Selected DH algorithms:")
        for algo, equiv in dh_security_levels:
            bits = extract_security_value(equiv)
            if bits:
                classification = get_security_classification(bits)
                status = "[OK]" if '[BROKEN]' not in equiv and '[DEPRECATED]' not in equiv else "[WEAK]"
                print(f"  {status} {classification:<15} {algo:28} {equiv}")
            else:
                print(f"  [UNKNOWN] {algo:28} {equiv}")
        print(f"[INFO] Minimum security level: {min_security_bits} bits ({min_classification})")
        
        try:
            with open(moduli_path, 'r') as f:
                lines = f.readlines()
        except Exception as e:
            print(f"[ERROR] Cannot read moduli file: {e}", file=sys.stderr)
            return None
        
        filtered_lines = []
        removed_count = 0
        kept_count = 0
        
        for line in lines:
            original_line = line.rstrip('\n')
            
            # Skip comments and empty lines
            if not original_line or original_line.startswith('#'):
                filtered_lines.append(line)
                continue
            
            # Parse moduli line: timestamp | type | tests | trials | size | generator | modulus
            fields = original_line.split()
            if len(fields) >= 5:
                try:
                    # Field 1 (index 1): type (should be 2 for DH)
                    dh_type = int(fields[1])
                    # Field 4 (index 4): size in bits
                    bit_size = int(fields[4])
                    
                    # Map bit size to security equivalent
                    equiv = get_dh_security_equivalent_by_size(bit_size)
                    equiv_bits = extract_security_value(equiv)
                    
                    if equiv_bits is None:
                        equiv_bits = 0
                    
                    # Keep only if security level meets or exceeds minimum
                    if equiv_bits >= min_security_bits:
                        filtered_lines.append(line)
                        kept_count += 1
                    else:
                        print(f"[REMOVE] {equiv:50} (< {min_security_bits}b minimum)")
                        removed_count += 1
                        
                except (ValueError, IndexError):
                    # Invalid format - skip
                    filtered_lines.append(line)
            else:
                filtered_lines.append(line)
        
        print(f"[INFO] Kept {kept_count} DH groups, removed {removed_count} weak groups")
        return "".join(filtered_lines)
    
    def save_moduli(self, moduli_content: str) -> bool:
        """
        Save filtered moduli file to /etc/ssh/moduli or current directory.
        
        Args:
            moduli_content: Filtered moduli content
            
        Returns:
            bool: True if successful
        """
        moduli_path = "/etc/ssh/moduli"
        
        try:
            # Try system directory
            with open(moduli_path, 'w') as f:
                f.write(moduli_content)
            print(f"[OK] Moduli saved: {moduli_path}")
            return True
        
        except PermissionError:
            # Fallback to current directory
            local_path = os.path.join(os.getcwd(), "moduli")
            try:
                with open(local_path, 'w') as f:
                    f.write(moduli_content)
                print(f"[WARNING] System write failed. Saved to: {local_path}")
                print(f"  Install with: sudo cp {local_path} {moduli_path}")
                return True
            except Exception as e:
                print(f"[ERROR] Failed to save moduli: {e}", file=sys.stderr)
                return False
        
        except Exception as e:
            print(f"[ERROR] Failed to save moduli: {e}", file=sys.stderr)
            return False
    
    def generate_config_content(self) -> str:
        """
        Generate SSH configuration file content with descriptions.
        
        Order of settings:
        1. Protocol (SSH protocol version)
        2. RequiredRSASize (RSA minimum key size)
        3. Remaining cryptographic algorithms (in alphabetical order)
        
        Returns:
            str: Configuration file content
        """
        lines = [
            "# SSH Cryptography Configuration",
            "# Auto-generated - generate_ssh_crypto_config.py",
            "# Generated: " + datetime.now().isoformat(),
            "",
        ]
        
        # ===== STEP 1: PROTOCOL =====
        if 'protocol-version' in self.selected_algorithms:
            algos = self.selected_algorithms['protocol-version']
            if algos:
                sshd_var = ALGORITHM_MAPPING.get('protocol-version', 'Protocol')
                description = CLAUSE_DESCRIPTIONS.get(sshd_var, "")
                
                if description:
                    lines.append(f"# {sshd_var}")
                    for desc_line in [description[i:i+80] for i in range(0, len(description), 80)]:
                        lines.append(f"# {desc_line}")
                    lines.append("#")
                else:
                    lines.append(f"# {sshd_var}")
                
                algo_line = f"{sshd_var} " + ",".join(algos)
                lines.append(algo_line)
                lines.append("")
        
        # ===== STEP 2: RequiredRSASize =====
        rsa_algorithms = [algo for algos in self.selected_algorithms.values() 
                         for algo in algos 
                         if algo.startswith(('ssh-rsa', 'rsa-sha2'))]
        
        if rsa_algorithms and self.required_rsa_size:
            lines.append("# RequiredRSASize")
            lines.append("# Enforce minimum RSA key size for server to accept RSA keys.")
            lines.append("# Default: 1024 (not recommended)")
            lines.append("# Recommended: >= 3072 (per NIST SP 800-56Ar3)")
            if self.required_rsa_size == 3072:
                lines.append("# Current setting: 3072-bit (128b security equivalent) - TRANSITIONAL")
            lines.append("#")
            lines.append(f"RequiredRSASize {self.required_rsa_size}")
            lines.append("")
        
        # ===== STEP 3: CRYPTOGRAPHIC ALGORITHMS =====
        # Skip protocol-version (already handled above), process remaining in alphabetical order
        for algo_type in sorted(self.selected_algorithms.keys()):
            if algo_type == 'protocol-version':
                continue  # Already processed
            
            algos = self.selected_algorithms[algo_type]
            
            if not algos:
                continue
            
            sshd_var = ALGORITHM_MAPPING.get(algo_type, algo_type)
            
            # Add clause description
            description = CLAUSE_DESCRIPTIONS.get(sshd_var, "")
            if description:
                lines.append(f"# {sshd_var}")
                # Wrap long descriptions
                for desc_line in [description[i:i+80] for i in range(0, len(description), 80)]:
                    lines.append(f"# {desc_line}")
                lines.append("#")
            else:
                lines.append(f"# {sshd_var}")
            
            # Add algorithm line
            algo_line = f"{sshd_var} " + ",".join(algos)
            lines.append(algo_line)
            lines.append("")
        
        return "\n".join(lines)
    
    def save_config(self, output_path: str = None) -> bool:
        """
        Save configuration file.
        
        Args:
            output_path: Output file path
            
        Returns:
            bool: True if successful
        """
        if output_path is None:
            output_path = "/etc/ssh/sshd_config.d/91-Cryptography.conf"
        
        config_content = self.generate_config_content()
        
        try:
            # Try system directory
            try:
                with open(output_path, 'w') as f:
                    f.write(config_content)
                print(f"\n[OK] Configuration saved: {output_path}")
                return True
            
            except PermissionError:
                # Fallback to current directory
                local_filename = Path(output_path).name
                local_path = os.path.join(os.getcwd(), local_filename)
                
                print(f"\n[WARNING] No write permission to {output_path}.")
                print(f"Saving to: {local_path}")
                
                with open(local_path, 'w') as f:
                    f.write(config_content)
                
                print(f"[OK] Configuration saved: {local_path}")
                print(f"\nInstall with:")
                print(f"  sudo cp {local_path} {output_path}")
                print(f"  sudo chmod 644 {output_path}")
                print(f"  sudo sshd -t  # Verify configuration")
                return True
        
        except Exception as e:
            print(f"[ERROR] Failed to save configuration: {e}", file=sys.stderr)
            return False
    
    def preview_config(self) -> None:
        """Display configuration file preview."""
        config_content = self.generate_config_content()
        print("\n" + "="*70)
        print("CONFIGURATION PREVIEW")
        print("="*70)
        print(config_content)
        print("="*70)


def main():
    """Main program function."""
    print("SSH Cryptography Configuration Generator")
    print("=========================================\n")
    
    # Create generator
    generator = SSHCryptoConfigGenerator()
    
    # Collect algorithms
    print("[INFO] Loading available SSH algorithms...")
    if not generator.fetch_ssh_algorithms():
        sys.exit(1)
    
    # Display available algorithms
    generator.display_algorithms()
    
    # Select protocol FIRST (STEP 1)
    generator._select_protocol_first()
    
    # Configure RSA security (STEP 2)
    print("\n" + "="*70)
    print("STEP 2: CONFIGURE RSA KEY SIZE")
    print("="*70)
    generator._configure_rsa_security_first()
    
    # Interactive selection of remaining algorithms (STEP 3)
    print("\n" + "="*70)
    print("STEP 3: SELECT REMAINING ALGORITHMS")
    print("="*70)
    if not generator.interactive_selection():
        sys.exit(1)
    
    # Validate RSA security after algorithm selection
    generator._configure_rsa_security()
    
    # Handle moduli file
    print("\n" + "="*70)
    print("DH MODULI MANAGEMENT")
    print("="*70)
    
    backup_success, backup_path = generator.backup_moduli_file()
    if backup_success:
        filtered_moduli = generator.filter_moduli_by_security(
            generator.selected_algorithms.get('kex', []),
            generator.selected_algorithms.get('kex-gss', [])
        )
        if filtered_moduli:
            generator.save_moduli(filtered_moduli)
    
    # Configuration preview
    generator.preview_config()
    
    # Confirmation and save
    while True:
        confirm = input("\nSave configuration? (yes/no/path): ").strip().lower()
        if confirm in ('yes', 'y', ''):
            if generator.save_config():
                print("\n[OK] Task completed!")
            break
        elif confirm in ('no', 'n'):
            print("Cancelled.")
            break
        elif confirm.startswith('/'):
            # Custom path
            if generator.save_config(confirm):
                print("\n[OK] Task completed!")
            break
        else:
            print("Please enter 'yes' or 'no'.")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n[INFO] Interrupted by user.")
        sys.exit(0)
