Instruction: Create an AUTARCH module in the 'defense' category that system audit with cve detection & auto-fix. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: mysystem
Category: defense
Description: System audit with CVE detection & auto-fix

Output:
"""
AUTARCH My System Module
Comprehensive system security audit with CVE detection and remediation

Performs full system audit, saves results, and offers LLM-assisted or manual fixes.
"""

import os
import sys
import json
import subprocess
import socket
from pathlib import Path
from datetime import datetime
from typing import Optional, List, Dict, Any

# Module metadata
DESCRIPTION = "System audit with CVE detection & auto-fix"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "defense"

sys.path.insert(0, str(Path(__file__).parent.parent))
from core.banner import Colors, clear_screen, display_banner
from core.config import get_config
from core.cve import get_cve_db

class SecurityIssue:
    """Represents a security issue found during audit."""

    SEVERITY_COLORS = {
        'CRITICAL': Colors.RED,
        'HIGH': Colors.RED,
        'MEDIUM': Colors.YELLOW,
        'LOW': Colors.CYAN,
        'INFO': Colors.DIM,
    }

    def __init__(
        self,
        name: str,
        description: str,
        severity: str,
        category: str,
        fix_command: str = None,
        fix_instructions: str = None,
        cve_ids: List[str] = None
    ):
        self.name = name
        self.description = description
        self.severity = severity.upper()
        self.category = category
        self.fix_command = fix_command
        self.fix_instructions = fix_instructions
        self.cve_ids = cve_ids or []
        self.status = "open"  # open, fixed, ignored

    def to_dict(self) -> Dict:
        return {
            'name': self.name,
            'description': self.description,
            'severity': self.severity,
            'category': self.category,
            'fix_command': self.fix_command,
            'fix_instructions': self.fix_instructions,
            'cve_ids': self.cve_ids,
            'status': self.status,
        }

    @classmethod
    def from_dict(cls, data: Dict) -> 'SecurityIssue':
        issue = cls(
            name=data.get('name', ''),
            description=data.get('description', ''),
            severity=data.get('severity', 'MEDIUM'),
            category=data.get('category', 'general'),
            fix_command=data.get('fix_command'),
            fix_instructions=data.get('fix_instructions'),
            cve_ids=data.get('cve_ids', []),
        )
        issue.status = data.get('status', 'open')
        return issue

class MySystem:
    """Comprehensive system security auditor."""

    @staticmethod
    def _system_inf_path():
        from core.paths import get_app_dir
        return get_app_dir() / "system.inf"

    def __init__(self):
        self.issues: List[SecurityIssue] = []
        self.system_info: Dict = {}
        self.audit_results: Dict = {}
        self.security_score: int = 100
        self.cve_db = get_cve_db()
        self.llm = None

    def print_status(self, message: str, status: str = "info"):
        colors = {
            "info": Colors.CYAN,
            "success": Colors.GREEN,
            "warning": Colors.YELLOW,
            "error": Colors.RED
        }
        symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
        print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")

    def run_cmd(self, cmd: str, timeout: int = 10) -> tuple:
        """Run command and return (success, output)."""
        try:
            result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
            return result.returncode == 0, result.stdout.strip()
        except:
            return False, ""

    def collect_system_info(self):
        """Collect comprehensive system information."""
        self.print_status("Collecting system information...")

        info = self.cve_db.get_system_info()

        # Additional system info
        success, output = self.run_cmd("hostname")
        info['hostname'] = output if success else 'unknown'

        success, output = self.run_cmd("uptime -p 2>/dev/null || uptime")
        info['uptime'] = output if success else 'unknown'

        success, output = self.run_cmd("whoami")
        info['current_user'] = output if success else 'unknown'

        success, output = self.run_cmd("cat /proc/meminfo 2>/dev/null | grep MemTotal | awk '{print $2}'")
        if success and output:
            info['memory_kb'] = int(output)
            info['memory_gb'] = round(int(output) / 1024 / 1024, 1)

        success, output = self.run_cmd("nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null")
        info['cpu_cores'] = int(output) if success and output.isdigit() else 0

        self.system_info = info

    def add_issue(
        self,
        name: str,
        description: str,
        severity: str,
        category: str,
        fix_command: str = None,
        fix_instructions: str = None,
        score_penalty: int = 5
    ):
        """Add a security issue to the list."""
        issue = SecurityIssue(
            name=name,
            description=description,
            severity=severity,
            category=category,
            fix_command=fix_command,
            fix_instructions=fix_instructions,
        )
        self.issues.append(issue)

        # Adjust score based on severity
        penalties = {'CRITICAL': 20, 'HIGH': 15, 'MEDIUM': 10, 'LOW': 5, 'INFO': 0}
        self.security_score -= penalties.get(severity.upper(), score_penalty)
        self.security_score = max(0, self.security_score)

    # =========================================================================
    # AUDIT CHECKS
    # =========================================================================

    def check_firewall(self):
        """Check firewall status."""
        self.print_status("Checking firewall...")

        # Check iptables
        success, output = self.run_cmd("iptables -L -n 2>/dev/null | head -20")
        if success and "Chain" in output:
            rules = output.count("\n")
            if rules > 5:
                self.audit_results['firewall'] = {'status': 'enabled', 'type': 'iptables', 'rules': rules}
                return
            else:
                self.add_issue(
                    "Firewall - Minimal Rules",
                    f"iptables has only {rules} rules configured",
                    "MEDIUM",
                    "network",
                    fix_instructions="Configure iptables with appropriate rules or use ufw/firewalld for easier management"
                )
                return

        # Check ufw
        success, output = self.run_cmd("ufw status 2>/dev/null")
        if success and "active" in output.lower():
            self.audit_results['firewall'] = {'status': 'enabled', 'type': 'ufw'}
            return

        # Check firewalld
        success, output = self.run_cmd("firewall-cmd --state 2>/dev/null")
        if success and "running" in output.lower():
            self.audit_results['firewall'] = {'status': 'enabled', 'type': 'firewalld'}
            return

        # No firewall
        self.add_issue(
            "No Active Firewall",
            "No firewall (iptables/ufw/firewalld) is currently active",
            "HIGH",
            "network",
            fix_command="sudo ufw enable",
            fix_instructions="Enable UFW: sudo ufw enable\nOr install: sudo apt install ufw && sudo ufw enable"
        )

    def check_ssh_config(self):
        """Check SSH hardening."""
        self.print_status("Checking SSH configuration...")

        ssh_config = Path("/etc/ssh/sshd_config")
        if not ssh_config.exists():
            self.audit_results['ssh'] = {'status': 'not_installed'}
            return

        try:
            content = ssh_config.read_text()
        except PermissionError:
            success, content = self.run_cmd("sudo cat /etc/ssh/sshd_config 2>/dev/null")
            if not success:
                self.audit_results['ssh'] = {'status': 'permission_denied'}
                return

        self.audit_results['ssh'] = {'status': 'installed', 'issues': []}

        # Check root login
        if "PermitRootLogin no" not in content and "PermitRootLogin prohibit-password" not in content:
            self.add_issue(
                "SSH Root Login Enabled",
                "Root login via SSH is not disabled",
                "HIGH",
                "ssh",
                fix_command="sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config && sudo systemctl restart sshd",
                fix_instructions="Edit /etc/ssh/sshd_config:\n  PermitRootLogin no\nThen restart: sudo systemctl restart sshd"
            )

        # Check password auth
        if "PasswordAuthentication no" not in content:
            self.add_issue(
                "SSH Password Auth Enabled",
                "Password authentication is enabled (key-based is more secure)",
                "MEDIUM",
                "ssh",
                fix_command="sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && sudo systemctl restart sshd",
                fix_instructions="Edit /etc/ssh/sshd_config:\n  PasswordAuthentication no\nEnsure you have SSH keys set up first!"
            )

        # Check protocol version
        if "Protocol 1" in content:
            self.add_issue(
                "SSH Protocol 1 Enabled",
                "Insecure SSH Protocol 1 is enabled",
                "CRITICAL",
                "ssh",
                fix_instructions="Remove 'Protocol 1' from /etc/ssh/sshd_config"
            )

    def check_open_ports(self):
        """Check for listening ports."""
        self.print_status("Scanning open ports...")

        success, output = self.run_cmd("ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null")
        if not success:
            return

        lines = [l for l in output.split('\n') if 'LISTEN' in l]
        self.audit_results['ports'] = {'listening': len(lines), 'high_risk': []}

        high_risk_ports = {
            '21': ('FTP', 'HIGH', 'FTP transmits credentials in plaintext'),
            '23': ('Telnet', 'CRITICAL', 'Telnet is unencrypted'),
            '69': ('TFTP', 'HIGH', 'TFTP has no authentication'),
            '111': ('RPC', 'MEDIUM', 'RPC can expose services'),
            '135': ('MS-RPC', 'HIGH', 'Windows RPC - potential attack vector'),
            '139': ('NetBIOS', 'HIGH', 'NetBIOS session service'),
            '445': ('SMB', 'HIGH', 'SMB - common attack target'),
            '512': ('rexec', 'CRITICAL', 'Insecure remote execution'),
            '513': ('rlogin', 'CRITICAL', 'Insecure remote login'),
            '514': ('rsh', 'CRITICAL', 'Insecure remote shell'),
            '1433': ('MSSQL', 'MEDIUM', 'Database port exposed'),
            '3306': ('MySQL', 'MEDIUM', 'Database port exposed'),
            '3389': ('RDP', 'HIGH', 'Remote Desktop exposed'),
            '5432': ('PostgreSQL', 'MEDIUM', 'Database port exposed'),
            '5900': ('VNC', 'HIGH', 'VNC often weakly configured'),
        }

        for line in lines:
            for port, (name, severity, desc) in high_risk_ports.items():
                if f':{port} ' in line or f':{port}\t' in line:
                    self.audit_results['ports']['high_risk'].append(port)
                    self.add_issue(
                        f"High-Risk Port Open: {port} ({name})",
                        desc,
                        severity,
                        "network",
                        fix_instructions=f"Disable the {name} service if not needed:\n  sudo systemctl stop <service>\n  sudo systemctl disable <service>"
                    )

    def check_users(self):
        """Check user security."""
        self.print_status("Checking user accounts...")

        self.audit_results['users'] = {'issues': []}

        # Users with UID 0
        success, output = self.run_cmd("awk -F: '$3 == 0 {print $1}' /etc/passwd")
        if success:
            uid0_users = [u for u in output.split('\n') if u]
            if len(uid0_users) > 1:
                extra_roots = [u for u in uid0_users if u != 'root']
                self.add_issue(
                    "Multiple Root Users",
                    f"Users with UID 0 besides root: {', '.join(extra_roots)}",
                    "CRITICAL",
                    "users",
                    fix_instructions="Review and remove extra UID 0 accounts:\n  sudo vipw\n  Change UID to non-zero or remove account"
                )

        # Empty passwords
        success, output = self.run_cmd("sudo awk -F: '($2 == \"\" ) {print $1}' /etc/shadow 2>/dev/null")
        if success and output:
            empty = [u for u in output.split('\n') if u]
            if empty:
                self.add_issue(
                    "Users with Empty Passwords",
                    f"Accounts without passwords: {', '.join(empty)}",
                    "CRITICAL",
                    "users",
                    fix_instructions=f"Set passwords for these users:\n  sudo passwd <username>\nOr lock the accounts:\n  sudo usermod -L <username>"
                )

        # Users with shells that shouldn't have them
        success, output = self.run_cmd("awk -F: '($7 != \"/usr/sbin/nologin\" && $7 != \"/bin/false\" && $7 != \"/sbin/nologin\") {print $1}' /etc/passwd")
        if success:
            shell_users = [u for u in output.split('\n') if u]
            self.audit_results['users']['shell_users'] = len(shell_users)

    def check_permissions(self):
        """Check critical file permissions."""
        self.print_status("Checking file permissions...")

        critical_files = [
            ("/etc/passwd", "644", "User database"),
            ("/etc/shadow", "640", "Password hashes"),
            ("/etc/group", "644", "Group database"),
            ("/etc/gshadow", "640", "Group passwords"),
            ("/etc/ssh/sshd_config", "600", "SSH configuration"),
            ("/root", "700", "Root home directory"),
            ("/etc/crontab", "600", "System crontab"),
        ]

        self.audit_results['permissions'] = {'checked': 0, 'issues': 0}

        for filepath, expected, desc in critical_files:
            p = Path(filepath)
            if p.exists():
                self.audit_results['permissions']['checked'] += 1
                try:
                    mode = oct(p.stat().st_mode)[-3:]
                    if int(mode) > int(expected):
                        self.audit_results['permissions']['issues'] += 1
                        self.add_issue(
                            f"Insecure Permissions: {filepath}",
                            f"{desc} has mode {mode} (should be {expected} or less)",
                            "MEDIUM",
                            "permissions",
                            fix_command=f"sudo chmod {expected} {filepath}",
                            fix_instructions=f"Fix permissions:\n  sudo chmod {expected} {filepath}"
                        )
                except:
                    pass

        # Check for world-writable directories
        success, output = self.run_cmd("find /etc -type f -perm -002 2>/dev/null | head -5")
        if success and output:
            files = output.split('\n')
            self.add_issue(
                "World-Writable Files in /etc",
                f"Found {len(files)} world-writable files in /etc",
                "HIGH",
                "permissions",
                fix_instructions="Review and fix permissions:\n  find /etc -type f -perm -002 -exec chmod o-w {} \\;"
            )

    def check_services(self):
        """Check for unnecessary/dangerous services."""
        self.print_status("Auditing services...")

        dangerous_services = [
            ("telnet", "Telnet server"),
            ("rsh", "Remote shell"),
            ("rlogin", "Remote login"),
            ("tftp", "TFTP server"),
            ("vsftpd", "FTP server"),
            ("proftpd", "FTP server"),
            ("pure-ftpd", "FTP server"),
        ]

        self.audit_results['services'] = {'dangerous_running': []}

        for svc, desc in dangerous_services:
            success, _ = self.run_cmd(f"systemctl is-active {svc} 2>/dev/null")
            if success:
                self.audit_results['services']['dangerous_running'].append(svc)
                self.add_issue(
                    f"Dangerous Service Running: {svc}",
                    f"{desc} is running",
                    "HIGH",
                    "services",
                    fix_command=f"sudo systemctl stop {svc} && sudo systemctl disable {svc}",
                    fix_instructions=f"Stop and disable {svc}:\n  sudo systemctl stop {svc}\n  sudo systemctl disable {svc}"
                )

    def check_updates(self):
        """Check for available updates."""
        self.print_status("Checking for updates...")

        self.audit_results['updates'] = {'available': 0, 'security': 0}

        os_id = self.system_info.get('os_id', '')

        if os_id in ['debian', 'ubuntu', 'kali', 'mint']:
            success, output = self.run_cmd("apt list --upgradable 2>/dev/null | grep -c upgradable || echo 0", timeout=30)
            if success and output.isdigit():
                count = int(output)
                self.audit_results['updates']['available'] = count
                if count > 50:
                    self.add_issue(
                        "Many Pending Updates",
                        f"{count} packages need updating",
                        "MEDIUM",
                        "updates",
                        fix_command="sudo apt update && sudo apt upgrade -y",
                        fix_instructions="Update system:\n  sudo apt update\n  sudo apt upgrade"
                    )

        elif os_id in ['fedora', 'rhel', 'centos', 'rocky', 'alma']:
            success, output = self.run_cmd("dnf check-update 2>/dev/null | wc -l", timeout=30)
            if success and output.isdigit():
                self.audit_results['updates']['available'] = int(output)

    def check_fail2ban(self):
        """Check fail2ban status."""
        self.print_status("Checking fail2ban...")

        success, output = self.run_cmd("systemctl is-active fail2ban 2>/dev/null")
        if success and "active" in output:
            self.audit_results['fail2ban'] = {'status': 'running'}
        else:
            success, _ = self.run_cmd("which fail2ban-client 2>/dev/null")
            if success:
                self.add_issue(
                    "Fail2Ban Not Running",
                    "Fail2ban is installed but not running",
                    "MEDIUM",
                    "services",
                    fix_command="sudo systemctl start fail2ban && sudo systemctl enable fail2ban",
                    fix_instructions="Start fail2ban:\n  sudo systemctl start fail2ban\n  sudo systemctl enable fail2ban"
                )
            else:
                self.add_issue(
                    "Fail2Ban Not Installed",
                    "Fail2ban is not installed (protects against brute-force)",
                    "LOW",
                    "services",
                    fix_command="sudo apt install fail2ban -y && sudo systemctl enable fail2ban && sudo systemctl start fail2ban",
                    fix_instructions="Install fail2ban:\n  sudo apt install fail2ban\n  sudo systemctl enable --now fail2ban"
                )

    def check_antivirus(self):
        """Check for antivirus."""
        self.print_status("Checking antivirus...")

        # Check ClamAV
        success, _ = self.run_cmd("which clamscan 2>/dev/null")
        if success:
            self.audit_results['antivirus'] = {'status': 'installed', 'type': 'clamav'}
            return

        # Check for other AV
        for av in ['sophos', 'eset', 'kaspersky']:
            success, _ = self.run_cmd(f"which {av} 2>/dev/null")
            if success:
                self.audit_results['antivirus'] = {'status': 'installed', 'type': av}
                return

        self.add_issue(
            "No Antivirus Installed",
            "No antivirus solution detected",
            "LOW",
            "security",
            fix_command="sudo apt install clamav clamav-daemon -y && sudo freshclam",
            fix_instructions="Install ClamAV:\n  sudo apt install clamav clamav-daemon\n  sudo freshclam"
        )

    def check_cves(self, verbose: bool = True):
        """Check for CVEs affecting this system using local SQLite database."""
        self.print_status("Checking CVE database for system vulnerabilities...")

        # Get database stats
        db_stats = self.cve_db.get_db_stats()

        if db_stats['total_cves'] == 0:
            if verbose:
                print(f"{Colors.YELLOW}[!] Local CVE database is empty. Searching online...{Colors.RESET}")
            # Fall back to online search
            cves = self.cve_db.search_online(
                cpe_name=self.cve_db.system_info.get('cpe_prefix', ''),
                days_back=365,
                max_results=100,
                verbose=verbose
            )
        else:
            if verbose:
                print(f"{Colors.DIM}    Local DB: {db_stats['total_cves']:,} CVEs | Last sync: {db_stats.get('last_sync', 'Never')[:10] if db_stats.get('last_sync') else 'Never'}{Colors.RESET}")
            # Use local database
            cves = self.cve_db.get_system_cves(max_results=100)

        self.audit_results['cves'] = {
            'total': len(cves),
            'critical': sum(1 for c in cves if c.get('severity') == 'CRITICAL'),
            'high': sum(1 for c in cves if c.get('severity') == 'HIGH'),
            'medium': sum(1 for c in cves if c.get('severity') == 'MEDIUM'),
            'low': sum(1 for c in cves if c.get('severity') == 'LOW'),
            'items': cves[:20],  # Keep top 20
            'db_stats': db_stats,
        }

        if verbose:
            print(f"{Colors.GREEN}[+] Found {len(cves)} CVEs for your system{Colors.RESET}")

        # Add critical/high CVEs as issues
        for cve in cves[:10]:
            if cve.get('severity') in ['CRITICAL', 'HIGH']:
                cve_id = cve.get('cve_id') or cve.get('id', '')
                desc = cve.get('description', '')[:150]
                self.add_issue(
                    f"CVE: {cve_id}",
                    desc,
                    cve['severity'],
                    "cve",
                    fix_instructions=f"Check: https://nvd.nist.gov/vuln/detail/{cve_id}\nUpdate affected packages and apply patches.",
                    score_penalty=15 if cve['severity'] == 'CRITICAL' else 10
                )

    # =========================================================================
    # MAIN AUDIT
    # =========================================================================

    def run_full_audit(self, check_cves: bool = True):
        """Run complete system audit."""
        clear_screen()
        display_banner()

        print(f"\n{Colors.BOLD}{Colors.CYAN}Starting Full System Security Audit{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        self.issues = []
        self.audit_results = {}
        self.security_score = 100

        # Collect info and run checks
        self.collect_system_info()
        self.check_firewall()
        self.check_ssh_config()
        self.check_open_ports()
        self.check_users()
        self.check_permissions()
        self.check_services()
        self.check_updates()
        self.check_fail2ban()
        self.check_antivirus()

        if check_cves:
            self.check_cves()

        print(f"\n{Colors.DIM}{'─' * 50}{Colors.RESET}")
        self.print_summary()

    def print_summary(self):
        """Print audit summary."""
        print(f"\n{Colors.BOLD}Security Audit Summary{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 40}{Colors.RESET}")

        # System info
        print(f"\n{Colors.CYAN}System:{Colors.RESET} {self.system_info.get('os_name', 'Unknown')}")
        print(f"{Colors.CYAN}Hostname:{Colors.RESET} {self.system_info.get('hostname', 'Unknown')}")

        # Issue counts by severity
        critical = sum(1 for i in self.issues if i.severity == 'CRITICAL')
        high = sum(1 for i in self.issues if i.severity == 'HIGH')
        medium = sum(1 for i in self.issues if i.severity == 'MEDIUM')
        low = sum(1 for i in self.issues if i.severity == 'LOW')

        print(f"\n{Colors.BOLD}Issues Found:{Colors.RESET}")
        if critical:
            print(f"  {Colors.RED}CRITICAL: {critical}{Colors.RESET}")
        if high:
            print(f"  {Colors.RED}HIGH: {high}{Colors.RESET}")
        if medium:
            print(f"  {Colors.YELLOW}MEDIUM: {medium}{Colors.RESET}")
        if low:
            print(f"  {Colors.CYAN}LOW: {low}{Colors.RESET}")

        if not self.issues:
            print(f"  {Colors.GREEN}No issues found!{Colors.RESET}")

        # Security score
        print(f"\n{Colors.BOLD}Security Score: ", end="")
        if self.security_score >= 80:
            print(f"{Colors.GREEN}{self.security_score}/100{Colors.RESET}")
        elif self.security_score >= 50:
            print(f"{Colors.YELLOW}{self.security_score}/100{Colors.RESET}")
        else:
            print(f"{Colors.RED}{self.security_score}/100{Colors.RESET}")

    def save_to_file(self) -> bool:
        """Save audit results to system.inf."""
        try:
            data = {
                'audit_date': datetime.now().isoformat(),
                'system_info': self.system_info,
                'security_score': self.security_score,
                'audit_results': self.audit_results,
                'issues': [i.to_dict() for i in self.issues],
            }

            with open(self._system_inf_path(), 'w') as f:
                json.dump(data, f, indent=2, default=str)

            self.print_status(f"Results saved to {self._system_inf_path()}", "success")
            return True

        except Exception as e:
            self.print_status(f"Failed to save: {e}", "error")
            return False

    def load_from_file(self) -> bool:
        """Load previous audit results from system.inf."""
        if not self._system_inf_path().exists():
            return False

        try:
            with open(self._system_inf_path(), 'r') as f:
                data = json.load(f)

            self.system_info = data.get('system_info', {})
            self.security_score = data.get('security_score', 100)
            self.audit_results = data.get('audit_results', {})
            self.issues = [SecurityIssue.from_dict(i) for i in data.get('issues', [])]

            return True

        except Exception as e:
            self.print_status(f"Failed to load: {e}", "error")
            return False

    # =========================================================================
    # ISSUE REMEDIATION
    # =========================================================================

    def show_issue_details(self, issue: SecurityIssue):
        """Show detailed information about an issue."""
        clear_screen()
        display_banner()

        color = SecurityIssue.SEVERITY_COLORS.get(issue.severity, Colors.WHITE)

        print(f"\n{Colors.BOLD}Issue Details{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        print(f"{Colors.BOLD}Name:{Colors.RESET} {issue.name}")
        print(f"{Colors.BOLD}Severity:{Colors.RESET} {color}{issue.severity}{Colors.RESET}")
        print(f"{Colors.BOLD}Category:{Colors.RESET} {issue.category}")
        print(f"\n{Colors.BOLD}Description:{Colors.RESET}")
        print(f"  {issue.description}")

        if issue.cve_ids:
            print(f"\n{Colors.BOLD}Related CVEs:{Colors.RESET}")
            for cve in issue.cve_ids:
                print(f"  - {cve}")

        if issue.fix_instructions:
            print(f"\n{Colors.BOLD}Manual Fix Instructions:{Colors.RESET}")
            for line in issue.fix_instructions.split('\n'):
                print(f"  {line}")

        if issue.fix_command:
            print(f"\n{Colors.BOLD}Auto-Fix Command:{Colors.RESET}")
            print(f"  {Colors.CYAN}{issue.fix_command}{Colors.RESET}")

        print(f"\n{Colors.DIM}{'─' * 50}{Colors.RESET}")

    def attempt_llm_fix(self, issue: SecurityIssue) -> bool:
        """Use LLM to generate and optionally apply a fix."""
        try:
            from core.llm import get_llm, LLMError
        except ImportError:
            self.print_status("LLM module not available", "error")
            return False

        self.print_status("Consulting LLM for fix recommendation...", "info")

        try:
            llm = get_llm()

            if not llm.is_loaded:
                self.print_status("Loading LLM model...", "info")
                llm.load_model(verbose=True)

            # Build prompt
            prompt = f"""You are a Linux security expert. Analyze this security issue and provide a fix.

System: {self.system_info.get('os_name', 'Linux')}
Issue: {issue.name}
Severity: {issue.severity}
Description: {issue.description}
Category: {issue.category}

Provide:
1. A brief explanation of the risk
2. The exact command(s) to fix this issue
3. Any important warnings or prerequisites

Format your response clearly with sections."""

            print(f"\n{Colors.CYAN}LLM Analysis:{Colors.RESET}\n")

            # Generate response with streaming
            response_text = ""
            for token in llm.generate(prompt, stream=True, max_tokens=500):
                print(token, end="", flush=True)
                response_text += token

            print("\n")

            # Ask if user wants to apply suggested fix
            if issue.fix_command:
                print(f"\n{Colors.YELLOW}Suggested command:{Colors.RESET} {issue.fix_command}")
                choice = input(f"\n{Colors.WHITE}Apply this fix? (y/n): {Colors.RESET}").strip().lower()

                if choice == 'y':
                    print(f"\n{Colors.CYAN}[*] Executing: {issue.fix_command}{Colors.RESET}")
                    success, output = self.run_cmd(issue.fix_command, timeout=60)

                    if success:
                        self.print_status("Fix applied successfully!", "success")
                        issue.status = "fixed"
                        return True
                    else:
                        self.print_status(f"Command failed: {output}", "error")
                        return False

            return True

        except Exception as e:
            self.print_status(f"LLM error: {e}", "error")
            return False

    def apply_manual_fix(self, issue: SecurityIssue) -> bool:
        """Apply the predefined fix command."""
        if not issue.fix_command:
            self.print_status("No automatic fix available for this issue", "warning")
            return False

        print(f"\n{Colors.BOLD}Fix Command:{Colors.RESET}")
        print(f"  {Colors.CYAN}{issue.fix_command}{Colors.RESET}")

        print(f"\n{Colors.YELLOW}Warning: This will modify your system.{Colors.RESET}")
        choice = input(f"{Colors.WHITE}Execute this command? (y/n): {Colors.RESET}").strip().lower()

        if choice != 'y':
            self.print_status("Fix cancelled", "info")
            return False

        print(f"\n{Colors.CYAN}[*] Executing...{Colors.RESET}")
        success, output = self.run_cmd(issue.fix_command, timeout=60)

        if output:
            print(f"\n{Colors.DIM}Output:{Colors.RESET}")
            print(output)

        if success:
            self.print_status("Fix applied successfully!", "success")
            issue.status = "fixed"
            return True
        else:
            self.print_status("Command failed", "error")
            return False

    # =========================================================================
    # MENU SYSTEM
    # =========================================================================

    def show_menu(self):
        """Display main menu."""
        clear_screen()
        display_banner()

        # Load previous results if available
        has_results = self._system_inf_path().exists()

        # Get CVE database stats
        db_stats = self.cve_db.get_db_stats()

        print(f"\n{Colors.BLUE}{Colors.BOLD}  My System - Security Audit{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")

        # Show system info
        sys_info = self.cve_db.get_system_info()
        print(f"\n  {Colors.CYAN}Detected:{Colors.RESET} {sys_info.get('os_id', 'unknown')} {sys_info.get('os_version', '')}")
        print(f"  {Colors.CYAN}Kernel:{Colors.RESET} {sys_info.get('kernel', 'unknown')}")

        # Show CVE database status
        if db_stats['total_cves'] > 0:
            last_sync = db_stats.get('last_sync', '')[:10] if db_stats.get('last_sync') else 'Never'
            print(f"  {Colors.CYAN}CVE Database:{Colors.RESET} {db_stats['total_cves']:,} CVEs ({db_stats['db_size_mb']} MB)")
            print(f"  {Colors.CYAN}Last Sync:{Colors.RESET} {last_sync}")
        else:
            print(f"  {Colors.YELLOW}CVE Database:{Colors.RESET} Empty - sync required")

        if has_results and self.issues:
            print(f"  {Colors.CYAN}Last Score:{Colors.RESET} {self.security_score}/100")
            print(f"  {Colors.CYAN}Open Issues:{Colors.RESET} {sum(1 for i in self.issues if i.status == 'open')}")

        print(f"\n{Colors.DIM}  {'─' * 50}{Colors.RESET}\n")

        print(f"  {Colors.GREEN}[1]{Colors.RESET} Run Full System Audit")
        print(f"  {Colors.GREEN}[2]{Colors.RESET} Run Audit (Skip CVE Check)")

        if has_results:
            print(f"\n  {Colors.CYAN}[3]{Colors.RESET} View Issues ({len(self.issues)} found)")
            print(f"  {Colors.CYAN}[4]{Colors.RESET} View CVE Report")

        print(f"\n  {Colors.YELLOW}[5]{Colors.RESET} Search CVE Database")
        print(f"  {Colors.YELLOW}[6]{Colors.RESET} Check Software for CVEs")

        print(f"\n  {Colors.MAGENTA}[7]{Colors.RESET} Sync CVE Database (Recent)")
        print(f"  {Colors.MAGENTA}[8]{Colors.RESET} Sync CVE Database (Full)")
        print(f"  {Colors.MAGENTA}[9]{Colors.RESET} CVE Database Info")

        print(f"\n  {Colors.DIM}[0]{Colors.RESET} Back to Main Menu")
        print()

    def show_issues_menu(self):
        """Display issues as selectable options."""
        if not self.issues:
            self.print_status("No issues found. Run an audit first.", "info")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        while True:
            clear_screen()
            display_banner()

            open_issues = [i for i in self.issues if i.status == 'open']
            fixed_issues = [i for i in self.issues if i.status == 'fixed']

            print(f"\n{Colors.BOLD}Security Issues{Colors.RESET}")
            print(f"{Colors.DIM}Score: {self.security_score}/100 | Open: {len(open_issues)} | Fixed: {len(fixed_issues)}{Colors.RESET}")
            print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")

            if not open_issues:
                print(f"{Colors.GREEN}All issues have been addressed!{Colors.RESET}")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
                return

            # List issues
            for idx, issue in enumerate(open_issues, 1):
                color = SecurityIssue.SEVERITY_COLORS.get(issue.severity, Colors.WHITE)
                severity_badge = f"{color}[{issue.severity[:4]}]{Colors.RESET}"
                print(f"  [{idx:2}] {severity_badge} {issue.name}")

            print(f"\n  {Colors.DIM}[0]{Colors.RESET} Back")
            print()

            try:
                choice = input(f"{Colors.WHITE}  Select issue to fix: {Colors.RESET}").strip()

                if choice == '0':
                    break

                if choice.isdigit():
                    idx = int(choice) - 1
                    if 0 <= idx < len(open_issues):
                        self.handle_issue(open_issues[idx])

            except (EOFError, KeyboardInterrupt):
                break

    def handle_issue(self, issue: SecurityIssue):
        """Handle remediation of a single issue."""
        self.show_issue_details(issue)

        print(f"\n  {Colors.GREEN}[1]{Colors.RESET} Auto-Fix with LLM")
        print(f"  {Colors.CYAN}[2]{Colors.RESET} Apply Manual Fix")
        print(f"  {Colors.YELLOW}[3]{Colors.RESET} Mark as Ignored")
        print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
        print()

        try:
            choice = input(f"{Colors.WHITE}  Select action: {Colors.RESET}").strip()

            if choice == '1':
                self.attempt_llm_fix(issue)
                self.save_to_file()
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            elif choice == '2':
                self.apply_manual_fix(issue)
                self.save_to_file()
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            elif choice == '3':
                issue.status = 'ignored'
                self.print_status("Issue marked as ignored", "info")
                self.save_to_file()
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

        except (EOFError, KeyboardInterrupt):
            pass

    def show_cve_report(self):
        """Show CVE report from audit."""
        clear_screen()
        display_banner()

        cve_data = self.audit_results.get('cves', {})

        print(f"\n{Colors.BOLD}CVE Report for Your System{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")

        print(f"  {Colors.CYAN}Total CVEs:{Colors.RESET} {cve_data.get('total', 0)}")
        print(f"  {Colors.RED}Critical:{Colors.RESET} {cve_data.get('critical', 0)}")
        print(f"  {Colors.RED}High:{Colors.RESET} {cve_data.get('high', 0)}")
        print(f"  {Colors.YELLOW}Medium:{Colors.RESET} {cve_data.get('medium', 0)}")
        print(f"  {Colors.CYAN}Low:{Colors.RESET} {cve_data.get('low', 0)}")

        print(f"\n{Colors.BOLD}Top CVEs:{Colors.RESET}\n")

        for cve in cve_data.get('items', [])[:15]:
            color = SecurityIssue.SEVERITY_COLORS.get(cve['severity'], Colors.WHITE)
            print(f"  {color}{cve['id']}{Colors.RESET} ({cve['severity']}) - CVSS: {cve['cvss_score']}")
            print(f"    {Colors.DIM}{cve['description'][:70]}...{Colors.RESET}")
            print()

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def search_cve_interactive(self):
        """Interactive CVE search."""
        clear_screen()
        display_banner()

        print(f"\n{Colors.BOLD}CVE Database Search{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        db_stats = self.cve_db.get_db_stats()
        if db_stats['total_cves'] > 0:
            print(f"{Colors.DIM}Local database: {db_stats['total_cves']:,} CVEs{Colors.RESET}\n")
        else:
            print(f"{Colors.YELLOW}Local database empty - will search online{Colors.RESET}\n")

        keyword = input(f"{Colors.WHITE}Search keyword (or Enter for system CVEs): {Colors.RESET}").strip()

        print()
        self.print_status("Searching CVE database...", "info")

        # Try local database first, fall back to online
        if db_stats['total_cves'] > 0:
            if keyword:
                cves = self.cve_db.search_cves(keyword=keyword)
            else:
                cves = self.cve_db.get_system_cves()
        else:
            if keyword:
                cves = self.cve_db.search_online(keyword=keyword, verbose=True)
            else:
                cve_prefix = self.cve_db.system_info.get('cpe_prefix', '')
                cves = self.cve_db.search_online(cpe_name=cve_prefix, verbose=True)

        if not cves:
            self.print_status("No CVEs found", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        print(f"\n{Colors.BOLD}Results ({len(cves)} found):{Colors.RESET}\n")

        for cve in cves[:20]:
            color = SecurityIssue.SEVERITY_COLORS.get(cve.get('severity', ''), Colors.WHITE)
            cve_id = cve.get('cve_id') or cve.get('id', '')
            score = cve.get('cvss_score') or cve.get('cvss_v3_score') or 0
            desc = cve.get('description', '')[:80]
            print(f"  {color}{cve_id}{Colors.RESET} - CVSS: {score} ({cve.get('severity', 'N/A')})")
            print(f"    {Colors.DIM}{desc}...{Colors.RESET}")
            print()

        # Option to view details
        cve_id = input(f"\n{Colors.WHITE}Enter CVE ID for details (or Enter to go back): {Colors.RESET}").strip().upper()

        if cve_id:
            # Try local first, then online
            details = self.cve_db.get_cve(cve_id)
            if not details:
                details = self.cve_db.fetch_cve_online(cve_id, verbose=True)
            if details:
                self.show_cve_details(details)
            else:
                self.print_status(f"CVE {cve_id} not found", "warning")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def show_cve_details(self, cve: Dict):
        """Show detailed CVE information."""
        clear_screen()
        display_banner()

        cve_id = cve.get('cve_id') or cve.get('id', '')
        print(f"\n{Colors.BOLD}{cve_id}{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")

        print(f"{Colors.BOLD}Description:{Colors.RESET}")
        print(f"  {cve.get('description', 'N/A')}\n")

        # CVSS v3
        if cve.get('cvss_v3_score'):
            color = SecurityIssue.SEVERITY_COLORS.get(cve.get('cvss_v3_severity', ''), Colors.WHITE)
            print(f"{Colors.BOLD}CVSS v3:{Colors.RESET}")
            print(f"  Score: {color}{cve['cvss_v3_score']} ({cve.get('cvss_v3_severity', 'N/A')}){Colors.RESET}")
            if cve.get('cvss_v3_vector'):
                print(f"  Vector: {cve['cvss_v3_vector']}")

        # CVSS v2 (if no v3)
        elif cve.get('cvss_v2_score'):
            color = SecurityIssue.SEVERITY_COLORS.get(cve.get('cvss_v2_severity', ''), Colors.WHITE)
            print(f"{Colors.BOLD}CVSS v2:{Colors.RESET}")
            print(f"  Score: {color}{cve['cvss_v2_score']} ({cve.get('cvss_v2_severity', 'N/A')}){Colors.RESET}")
            if cve.get('cvss_v2_vector'):
                print(f"  Vector: {cve['cvss_v2_vector']}")

        if cve.get('published'):
            print(f"\n{Colors.BOLD}Published:{Colors.RESET} {cve['published'][:10]}")

        if cve.get('weaknesses'):
            print(f"\n{Colors.BOLD}Weaknesses (CWE):{Colors.RESET}")
            for w in cve['weaknesses'][:5]:
                print(f"  - {w}")

        if cve.get('references'):
            print(f"\n{Colors.BOLD}References:{Colors.RESET}")
            for ref in cve['references'][:5]:
                url = ref.get('url', ref) if isinstance(ref, dict) else ref
                print(f"  - {url}")

        if cve.get('cpes'):
            print(f"\n{Colors.BOLD}Affected Products:{Colors.RESET}")
            for cpe in cve['cpes'][:5]:
                criteria = cpe.get('cpe_criteria', cpe) if isinstance(cpe, dict) else cpe
                print(f"  - {criteria}")

        print(f"\n{Colors.CYAN}Full details: https://nvd.nist.gov/vuln/detail/{cve_id}{Colors.RESET}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def check_software_cves(self):
        """Check CVEs for specific software."""
        clear_screen()
        display_banner()

        print(f"\n{Colors.BOLD}Software CVE Check{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        db_stats = self.cve_db.get_db_stats()
        if db_stats['total_cves'] > 0:
            print(f"{Colors.DIM}Local database: {db_stats['total_cves']:,} CVEs{Colors.RESET}\n")

        software = input(f"{Colors.WHITE}Software name (e.g., apache, nginx, openssh): {Colors.RESET}").strip()
        if not software:
            return

        version = input(f"{Colors.WHITE}Version (optional): {Colors.RESET}").strip()

        print()
        self.print_status(f"Searching CVEs for {software}...", "info")

        # Try local database first
        if db_stats['total_cves'] > 0:
            cves = self.cve_db.get_software_cves(software, version=version if version else None)
        else:
            # Fall back to online search
            keyword = f"{software} {version}" if version else software
            cves = self.cve_db.search_online(keyword=keyword, verbose=True)

        if not cves:
            self.print_status("No CVEs found for this software", "success")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        print(f"\n{Colors.BOLD}Found {len(cves)} CVEs:{Colors.RESET}\n")

        for cve in cves[:15]:
            color = SecurityIssue.SEVERITY_COLORS.get(cve.get('severity', ''), Colors.WHITE)
            cve_id = cve.get('cve_id') or cve.get('id', '')
            score = cve.get('cvss_score') or cve.get('cvss_v3_score') or 0
            desc = cve.get('description', '')
            desc = desc[:70] + '...' if len(desc) > 70 else desc
            print(f"  {color}{cve_id}{Colors.RESET} - CVSS: {score} ({cve.get('severity', 'N/A')})")
            print(f"    {Colors.DIM}{desc}{Colors.RESET}")
            print()

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def sync_database_recent(self):
        """Sync recent CVEs (last 120 days)."""
        clear_screen()
        display_banner()

        print(f"\n{Colors.BOLD}CVE Database Sync (Recent){Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        print(f"This will download CVEs from the last 120 days.")
        print(f"Estimated time: 5-15 minutes (depending on API rate limits)\n")

        confirm = input(f"{Colors.WHITE}Start sync? (y/n): {Colors.RESET}").strip().lower()
        if confirm != 'y':
            return

        print()
        stats = self.cve_db.sync_database(days_back=120, verbose=True)

        print(f"\n{Colors.BOLD}Sync Complete{Colors.RESET}")
        print(f"  CVEs processed: {stats.get('cves_processed', 0):,}")
        print(f"  Errors: {stats.get('errors', 0)}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def sync_database_full(self):
        """Full database sync (all CVEs since 1999)."""
        clear_screen()
        display_banner()

        print(f"\n{Colors.BOLD}CVE Database Sync (Full){Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        print(f"{Colors.YELLOW}WARNING: This will download ALL CVEs (200,000+){Colors.RESET}")
        print(f"Estimated time: 2-6 hours (depending on API rate limits)")
        print(f"Database size: ~150-300 MB\n")

        print(f"Consider getting an NVD API key for faster sync:")
        print(f"  https://nvd.nist.gov/developers/request-an-api-key\n")

        confirm = input(f"{Colors.WHITE}Start full sync? (y/n): {Colors.RESET}").strip().lower()
        if confirm != 'y':
            return

        print()
        stats = self.cve_db.sync_database(full_sync=True, verbose=True)

        print(f"\n{Colors.BOLD}Sync Complete{Colors.RESET}")
        print(f"  CVEs processed: {stats.get('cves_processed', 0):,}")
        print(f"  Errors: {stats.get('errors', 0)}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def show_database_info(self):
        """Show CVE database information."""
        clear_screen()
        display_banner()

        print(f"\n{Colors.BOLD}CVE Database Information{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        stats = self.cve_db.get_db_stats()

        print(f"  {Colors.CYAN}Database Path:{Colors.RESET} {stats['db_path']}")
        print(f"  {Colors.CYAN}Database Size:{Colors.RESET} {stats['db_size_mb']} MB")
        print(f"  {Colors.CYAN}Total CVEs:{Colors.RESET} {stats['total_cves']:,}")
        print(f"  {Colors.CYAN}Total CPEs:{Colors.RESET} {stats['total_cpes']:,}")
        print(f"  {Colors.CYAN}Last Sync:{Colors.RESET} {stats.get('last_sync', 'Never')}")

        if stats.get('by_severity'):
            print(f"\n  {Colors.BOLD}CVEs by Severity:{Colors.RESET}")
            for sev, count in sorted(stats['by_severity'].items()):
                color = SecurityIssue.SEVERITY_COLORS.get(sev, Colors.WHITE)
                print(f"    {color}{sev}:{Colors.RESET} {count:,}")

        sys_info = self.cve_db.get_system_info()
        print(f"\n  {Colors.BOLD}System Detection:{Colors.RESET}")
        print(f"    OS: {sys_info.get('os_name', 'Unknown')}")
        print(f"    CPE: {sys_info.get('cpe_prefix', 'Unknown')}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def run(self):
        """Main module loop."""
        # Try to load previous results
        self.load_from_file()

        while True:
            self.show_menu()

            try:
                choice = input(f"{Colors.WHITE}  Select: {Colors.RESET}").strip()

                if choice == '0':
                    break

                elif choice == '1':
                    self.run_full_audit(check_cves=True)
                    self.save_to_file()
                    input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

                elif choice == '2':
                    self.run_full_audit(check_cves=False)
                    self.save_to_file()
                    input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

                elif choice == '3' and self._system_inf_path().exists():
                    self.show_issues_menu()

                elif choice == '4' and self._system_inf_path().exists():
                    self.show_cve_report()

                elif choice == '5':
                    self.search_cve_interactive()

                elif choice == '6':
                    self.check_software_cves()

                elif choice == '7':
                    self.sync_database_recent()

                elif choice == '8':
                    self.sync_database_full()

                elif choice == '9':
                    self.show_database_info()

            except (EOFError, KeyboardInterrupt):
                break

def run():
    MySystem().run()

if __name__ == "__main__":
    run()


Instruction: Create an AUTARCH module in the 'analyze' category that network topology discovery & mapping. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: net_mapper
Category: analyze
Description: Network topology discovery & mapping

Output:
"""AUTARCH Network Topology Mapper

Host discovery, service enumeration, OS fingerprinting, and visual
network topology mapping with scan diffing.
"""

DESCRIPTION = "Network topology discovery & mapping"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "analyze"

import os
import re
import json
import time
import socket
import struct
import threading
import subprocess
from pathlib import Path
from datetime import datetime, timezone
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field

try:
    from core.paths import find_tool, get_data_dir
except ImportError:
    import shutil
    def find_tool(name):
        return shutil.which(name)
    def get_data_dir():
        return str(Path(__file__).parent.parent / 'data')

@dataclass
class Host:
    ip: str
    mac: str = ''
    hostname: str = ''
    os_guess: str = ''
    ports: List[dict] = field(default_factory=list)
    state: str = 'up'
    subnet: str = ''

    def to_dict(self) -> dict:
        return {
            'ip': self.ip, 'mac': self.mac, 'hostname': self.hostname,
            'os_guess': self.os_guess, 'ports': self.ports,
            'state': self.state, 'subnet': self.subnet,
        }

class NetMapper:
    """Network topology discovery and mapping."""

    def __init__(self):
        self._data_dir = os.path.join(get_data_dir(), 'net_mapper')
        os.makedirs(self._data_dir, exist_ok=True)
        self._active_jobs: Dict[str, dict] = {}

    # ── Host Discovery ────────────────────────────────────────────────────

    def discover_hosts(self, target: str, method: str = 'auto',
                       timeout: float = 3.0) -> dict:
        """Discover live hosts on a network.

        target: IP, CIDR (192.168.1.0/24), or range (192.168.1.1-254)
        method: 'arp', 'icmp', 'tcp', 'nmap', 'auto'
        """
        job_id = f'discover_{int(time.time())}'
        holder = {'done': False, 'hosts': [], 'error': None}
        self._active_jobs[job_id] = holder

        def do_discover():
            try:
                nmap = find_tool('nmap')
                if method == 'nmap' or (method == 'auto' and nmap):
                    hosts = self._nmap_discover(target, nmap, timeout)
                elif method == 'icmp' or method == 'auto':
                    hosts = self._ping_sweep(target, timeout)
                elif method == 'tcp':
                    hosts = self._tcp_discover(target, timeout)
                else:
                    hosts = self._ping_sweep(target, timeout)
                holder['hosts'] = [h.to_dict() for h in hosts]
            except Exception as e:
                holder['error'] = str(e)
            finally:
                holder['done'] = True

        threading.Thread(target=do_discover, daemon=True).start()
        return {'ok': True, 'job_id': job_id}

    def _nmap_discover(self, target: str, nmap: str, timeout: float) -> List[Host]:
        """Discover hosts using nmap."""
        cmd = [nmap, '-sn', '-PE', '-PA21,22,80,443,445,3389', '-oX', '-', target]
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
            return self._parse_nmap_xml(result.stdout)
        except Exception:
            return []

    def _ping_sweep(self, target: str, timeout: float) -> List[Host]:
        """ICMP ping sweep."""
        ips = self._expand_target(target)
        hosts = []
        lock = threading.Lock()

        def ping(ip):
            try:
                # Use socket instead of subprocess for speed
                s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.settimeout(timeout)
                # Try common ports to detect hosts even if ICMP is blocked
                for port in (80, 443, 22, 445):
                    try:
                        r = s.connect_ex((ip, port))
                        if r == 0:
                            h = Host(ip=ip, state='up',
                                     subnet='.'.join(ip.split('.')[:3]) + '.0/24')
                            try:
                                h.hostname = socket.getfqdn(ip)
                                if h.hostname == ip:
                                    h.hostname = ''
                            except Exception:
                                pass
                            with lock:
                                hosts.append(h)
                            s.close()
                            return
                    except Exception:
                        pass
                s.close()
            except Exception:
                pass

        threads = []
        for ip in ips:
            t = threading.Thread(target=ping, args=(ip,), daemon=True)
            threads.append(t)
            t.start()
            if len(threads) >= 100:
                for t in threads:
                    t.join(timeout=timeout + 2)
                threads.clear()
        for t in threads:
            t.join(timeout=timeout + 2)

        return sorted(hosts, key=lambda h: [int(x) for x in h.ip.split('.')])

    def _tcp_discover(self, target: str, timeout: float) -> List[Host]:
        """TCP SYN scan for discovery."""
        return self._ping_sweep(target, timeout)  # Same logic for now

    # ── Port Scanning ─────────────────────────────────────────────────────

    def scan_host(self, ip: str, port_range: str = '1-1024',
                  service_detection: bool = True,
                  os_detection: bool = True) -> dict:
        """Detailed scan of a single host."""
        nmap = find_tool('nmap')
        if nmap:
            return self._nmap_scan_host(ip, nmap, port_range,
                                        service_detection, os_detection)
        return self._socket_scan_host(ip, port_range)

    def _nmap_scan_host(self, ip: str, nmap: str, port_range: str,
                        svc: bool, os_det: bool) -> dict:
        cmd = [nmap, '-Pn', '-p', port_range, '-oX', '-', ip]
        if svc:
            cmd.insert(2, '-sV')
        if os_det:
            cmd.insert(2, '-O')
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
            hosts = self._parse_nmap_xml(result.stdout)
            if hosts:
                return {'ok': True, 'host': hosts[0].to_dict(), 'raw': result.stdout}
            return {'ok': True, 'host': Host(ip=ip, state='unknown').to_dict()}
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    def _socket_scan_host(self, ip: str, port_range: str) -> dict:
        """Fallback socket-based port scan."""
        start_port, end_port = 1, 1024
        if '-' in port_range:
            parts = port_range.split('-')
            start_port, end_port = int(parts[0]), int(parts[1])

        open_ports = []
        for port in range(start_port, min(end_port + 1, 65536)):
            try:
                s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.settimeout(1)
                if s.connect_ex((ip, port)) == 0:
                    open_ports.append({
                        'port': port, 'protocol': 'tcp', 'state': 'open',
                        'service': self._guess_service(port),
                    })
                s.close()
            except Exception:
                pass

        host = Host(ip=ip, state='up', ports=open_ports,
                    subnet='.'.join(ip.split('.')[:3]) + '.0/24')
        return {'ok': True, 'host': host.to_dict()}

    # ── Topology / Scan Management ────────────────────────────────────────

    def save_scan(self, name: str, hosts: List[dict]) -> dict:
        """Save a network scan for later comparison."""
        scan = {
            'name': name,
            'timestamp': datetime.now(timezone.utc).isoformat(),
            'hosts': hosts,
            'host_count': len(hosts),
        }
        path = os.path.join(self._data_dir, f'scan_{name}_{int(time.time())}.json')
        with open(path, 'w') as f:
            json.dump(scan, f, indent=2)
        return {'ok': True, 'path': path}

    def list_scans(self) -> List[dict]:
        scans = []
        for f in Path(self._data_dir).glob('scan_*.json'):
            try:
                with open(f, 'r') as fh:
                    data = json.load(fh)
                    scans.append({
                        'file': f.name,
                        'name': data.get('name', ''),
                        'timestamp': data.get('timestamp', ''),
                        'host_count': data.get('host_count', 0),
                    })
            except Exception:
                continue
        return sorted(scans, key=lambda s: s.get('timestamp', ''), reverse=True)

    def load_scan(self, filename: str) -> Optional[dict]:
        path = os.path.join(self._data_dir, filename)
        if os.path.exists(path):
            with open(path, 'r') as f:
                return json.load(f)
        return None

    def diff_scans(self, scan1_file: str, scan2_file: str) -> dict:
        """Compare two scans and find differences."""
        s1 = self.load_scan(scan1_file)
        s2 = self.load_scan(scan2_file)
        if not s1 or not s2:
            return {'ok': False, 'error': 'Scan(s) not found'}

        ips1 = {h['ip'] for h in s1.get('hosts', [])}
        ips2 = {h['ip'] for h in s2.get('hosts', [])}

        return {
            'ok': True,
            'new_hosts': sorted(ips2 - ips1),
            'removed_hosts': sorted(ips1 - ips2),
            'unchanged_hosts': sorted(ips1 & ips2),
            'scan1': {'name': s1.get('name'), 'timestamp': s1.get('timestamp'),
                      'count': len(ips1)},
            'scan2': {'name': s2.get('name'), 'timestamp': s2.get('timestamp'),
                      'count': len(ips2)},
        }

    def get_job_status(self, job_id: str) -> dict:
        holder = self._active_jobs.get(job_id)
        if not holder:
            return {'ok': False, 'error': 'Job not found'}
        result = {'ok': True, 'done': holder['done'], 'hosts': holder['hosts']}
        if holder.get('error'):
            result['error'] = holder['error']
        if holder['done']:
            self._active_jobs.pop(job_id, None)
        return result

    # ── Topology Data (for visualization) ─────────────────────────────────

    def build_topology(self, hosts: List[dict]) -> dict:
        """Build topology graph data from host list for visualization."""
        nodes = []
        edges = []
        subnets = {}

        for h in hosts:
            subnet = '.'.join(h['ip'].split('.')[:3]) + '.0/24'
            if subnet not in subnets:
                subnets[subnet] = {
                    'id': f'subnet_{subnet}', 'label': subnet,
                    'type': 'subnet', 'hosts': [],
                }
            subnets[subnet]['hosts'].append(h['ip'])

            node_type = 'host'
            if h.get('ports'):
                services = [p.get('service', '') for p in h['ports']]
                if any('http' in s.lower() for s in services):
                    node_type = 'web'
                elif any('ssh' in s.lower() for s in services):
                    node_type = 'server'
                elif any('smb' in s.lower() or 'netbios' in s.lower() for s in services):
                    node_type = 'windows'

            nodes.append({
                'id': h['ip'],
                'label': h.get('hostname') or h['ip'],
                'ip': h['ip'],
                'type': node_type,
                'os': h.get('os_guess', ''),
                'ports': len(h.get('ports', [])),
                'subnet': subnet,
            })

            # Edge from host to subnet gateway
            gateway = '.'.join(h['ip'].split('.')[:3]) + '.1'
            edges.append({'from': h['ip'], 'to': gateway, 'type': 'network'})

        # Add subnet nodes
        for subnet_data in subnets.values():
            nodes.append(subnet_data)

        return {
            'nodes': nodes,
            'edges': edges,
            'subnets': list(subnets.keys()),
            'total_hosts': len(hosts),
        }

    # ── Helpers ───────────────────────────────────────────────────────────

    def _expand_target(self, target: str) -> List[str]:
        """Expand CIDR or range to list of IPs."""
        if '/' in target:
            return self._cidr_to_ips(target)
        if '-' in target.split('.')[-1]:
            base = '.'.join(target.split('.')[:3])
            range_part = target.split('.')[-1]
            if '-' in range_part:
                start, end = range_part.split('-')
                return [f'{base}.{i}' for i in range(int(start), int(end) + 1)]
        return [target]

    @staticmethod
    def _cidr_to_ips(cidr: str) -> List[str]:
        parts = cidr.split('/')
        if len(parts) != 2:
            return [cidr]
        ip = parts[0]
        prefix = int(parts[1])
        if prefix < 16:
            return [ip]  # Too large, don't expand
        ip_int = struct.unpack('!I', socket.inet_aton(ip))[0]
        mask = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
        network = ip_int & mask
        broadcast = network | (~mask & 0xFFFFFFFF)
        return [socket.inet_ntoa(struct.pack('!I', i))
                for i in range(network + 1, broadcast)]

    def _parse_nmap_xml(self, xml_text: str) -> List[Host]:
        """Parse nmap XML output to Host objects."""
        hosts = []
        try:
            import xml.etree.ElementTree as ET
            root = ET.fromstring(xml_text)
            for host_el in root.findall('.//host'):
                state = host_el.find('status')
                if state is not None and state.get('state') != 'up':
                    continue
                addr = host_el.find("address[@addrtype='ipv4']")
                if addr is None:
                    continue
                ip = addr.get('addr', '')
                mac_el = host_el.find("address[@addrtype='mac']")
                hostname_el = host_el.find('.//hostname')
                os_el = host_el.find('.//osmatch')

                h = Host(
                    ip=ip,
                    mac=mac_el.get('addr', '') if mac_el is not None else '',
                    hostname=hostname_el.get('name', '') if hostname_el is not None else '',
                    os_guess=os_el.get('name', '') if os_el is not None else '',
                    subnet='.'.join(ip.split('.')[:3]) + '.0/24',
                )

                for port_el in host_el.findall('.//port'):
                    state_el = port_el.find('state')
                    if state_el is not None and state_el.get('state') == 'open':
                        svc_el = port_el.find('service')
                        h.ports.append({
                            'port': int(port_el.get('portid', 0)),
                            'protocol': port_el.get('protocol', 'tcp'),
                            'state': 'open',
                            'service': svc_el.get('name', '') if svc_el is not None else '',
                            'version': svc_el.get('version', '') if svc_el is not None else '',
                        })
                hosts.append(h)
        except Exception:
            pass
        return hosts

    @staticmethod
    def _guess_service(port: int) -> str:
        services = {
            21: 'ftp', 22: 'ssh', 23: 'telnet', 25: 'smtp', 53: 'dns',
            80: 'http', 110: 'pop3', 143: 'imap', 443: 'https', 445: 'smb',
            993: 'imaps', 995: 'pop3s', 3306: 'mysql', 3389: 'rdp',
            5432: 'postgresql', 5900: 'vnc', 6379: 'redis', 8080: 'http-alt',
            8443: 'https-alt', 27017: 'mongodb',
        }
        return services.get(port, '')

# ── Singleton ─────────────────────────────────────────────────────────────────

_instance = None
_lock = threading.Lock()

def get_net_mapper() -> NetMapper:
    global _instance
    if _instance is None:
        with _lock:
            if _instance is None:
                _instance = NetMapper()
    return _instance

# ── CLI ───────────────────────────────────────────────────────────────────────

def run():
    """Interactive CLI for Network Mapper."""
    svc = get_net_mapper()

    while True:
        print("\n╔═══════════════════════════════════════╗")
        print("║       NETWORK TOPOLOGY MAPPER         ║")
        print("╠═══════════════════════════════════════╣")
        print("║  1 — Discover Hosts                   ║")
        print("║  2 — Scan Host (detailed)             ║")
        print("║  3 — List Saved Scans                 ║")
        print("║  4 — Compare Scans                    ║")
        print("║  0 — Back                             ║")
        print("╚═══════════════════════════════════════╝")

        choice = input("\n  Select: ").strip()

        if choice == '0':
            break
        elif choice == '1':
            target = input("  Target (CIDR/range): ").strip()
            if not target:
                continue
            print("  Discovering hosts...")
            r = svc.discover_hosts(target)
            if r.get('job_id'):
                while True:
                    time.sleep(2)
                    s = svc.get_job_status(r['job_id'])
                    if s['done']:
                        hosts = s['hosts']
                        print(f"\n  Found {len(hosts)} hosts:")
                        for h in hosts:
                            ports = len(h.get('ports', []))
                            print(f"    {h['ip']:16s} {h.get('hostname',''):20s} "
                                  f"{h.get('os_guess',''):20s} {ports} ports")
                        save = input("\n  Save scan? (name/empty=skip): ").strip()
                        if save:
                            svc.save_scan(save, hosts)
                            print(f"  Saved as: {save}")
                        break
        elif choice == '2':
            ip = input("  Host IP: ").strip()
            if not ip:
                continue
            print("  Scanning...")
            r = svc.scan_host(ip)
            if r.get('ok'):
                h = r['host']
                print(f"\n  {h['ip']} — {h.get('os_guess', 'unknown OS')}")
                for p in h.get('ports', []):
                    print(f"    {p['port']:6d}/{p['protocol']}  {p.get('service','')}"
                          f"  {p.get('version','')}")
        elif choice == '3':
            scans = svc.list_scans()
            if not scans:
                print("\n  No saved scans.")
                continue
            for s in scans:
                print(f"  {s['file']:40s} {s['name']:15s} "
                      f"{s['host_count']} hosts  {s['timestamp'][:19]}")
        elif choice == '4':
            scans = svc.list_scans()
            if len(scans) < 2:
                print("  Need at least 2 saved scans.")
                continue
            for i, s in enumerate(scans, 1):
                print(f"  {i}. {s['file']} ({s['host_count']} hosts)")
            a = int(input("  Scan 1 #: ").strip()) - 1
            b = int(input("  Scan 2 #: ").strip()) - 1
            diff = svc.diff_scans(scans[a]['file'], scans[b]['file'])
            if diff.get('ok'):
                print(f"\n  New hosts:     {len(diff['new_hosts'])}")
                for h in diff['new_hosts']:
                    print(f"    + {h}")
                print(f"  Removed hosts: {len(diff['removed_hosts'])}")
                for h in diff['removed_hosts']:
                    print(f"    - {h}")
                print(f"  Unchanged:     {len(diff['unchanged_hosts'])}")


Instruction: Create an AUTARCH module in the 'utility' category that test network speed and connectivity. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: nettest
Category: utility
Description: Test network speed and connectivity

Output:
"""
AUTARCH Network Test Module
Test network speed and connectivity
"""

import sys
import time
from pathlib import Path

# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

from core.banner import Colors

# Module metadata
NAME = "Network Test"
DESCRIPTION = "Test network speed and connectivity"
AUTHOR = "darkHal Security Group"
VERSION = "1.0"
CATEGORY = "utility"

# Try to import optional dependencies
try:
    import speedtest
    HAS_SPEEDTEST = True
except ImportError:
    HAS_SPEEDTEST = False

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

class NetworkTester:
    """Network testing utility."""

    def __init__(self):
        self.test_urls = [
            ("Google", "https://www.google.com"),
            ("Cloudflare", "https://1.1.1.1"),
            ("GitHub", "https://github.com"),
            ("Amazon", "https://aws.amazon.com"),
        ]

    def test_connectivity(self) -> dict:
        """Test basic internet connectivity.

        Returns:
            Dict with connectivity results.
        """
        if not HAS_REQUESTS:
            return {'error': 'requests library not available'}

        print(f"{Colors.CYAN}[*] Testing connectivity...{Colors.RESET}")

        results = {
            'tests': [],
            'success_count': 0,
            'fail_count': 0,
        }

        for name, url in self.test_urls:
            try:
                start = time.time()
                response = requests.get(url, timeout=10)
                elapsed = round((time.time() - start) * 1000)

                success = response.status_code == 200
                results['tests'].append({
                    'name': name,
                    'url': url,
                    'success': success,
                    'status': response.status_code,
                    'time_ms': elapsed,
                })

                if success:
                    results['success_count'] += 1
                    print(f"  {Colors.GREEN}[+]{Colors.RESET} {name}: {elapsed}ms")
                else:
                    results['fail_count'] += 1
                    print(f"  {Colors.RED}[-]{Colors.RESET} {name}: HTTP {response.status_code}")

            except requests.exceptions.Timeout:
                results['fail_count'] += 1
                results['tests'].append({
                    'name': name,
                    'url': url,
                    'success': False,
                    'error': 'Timeout',
                })
                print(f"  {Colors.RED}[-]{Colors.RESET} {name}: Timeout")

            except requests.exceptions.ConnectionError:
                results['fail_count'] += 1
                results['tests'].append({
                    'name': name,
                    'url': url,
                    'success': False,
                    'error': 'Connection failed',
                })
                print(f"  {Colors.RED}[-]{Colors.RESET} {name}: Connection failed")

            except Exception as e:
                results['fail_count'] += 1
                results['tests'].append({
                    'name': name,
                    'url': url,
                    'success': False,
                    'error': str(e),
                })
                print(f"  {Colors.RED}[-]{Colors.RESET} {name}: {str(e)}")

        return results

    def test_speed(self) -> dict:
        """Test network speed using speedtest.

        Returns:
            Dict with speed test results.
        """
        if not HAS_SPEEDTEST:
            return {'error': 'speedtest-cli library not available'}

        print(f"{Colors.CYAN}[*] Running speed test (this may take a minute)...{Colors.RESET}")

        try:
            st = speedtest.Speedtest(secure=True)

            print(f"  {Colors.DIM}Finding best server...{Colors.RESET}")
            st.get_best_server()

            print(f"  {Colors.DIM}Testing download speed...{Colors.RESET}")
            st.download(threads=None)

            print(f"  {Colors.DIM}Testing upload speed...{Colors.RESET}")
            st.upload(threads=None)

            results = st.results.dict()

            return {
                'download_mbps': round(results['download'] / 1_000_000, 2),
                'upload_mbps': round(results['upload'] / 1_000_000, 2),
                'ping_ms': round(results['ping']),
                'client': {
                    'ip': results.get('client', {}).get('ip'),
                    'isp': results.get('client', {}).get('isp'),
                    'country': results.get('client', {}).get('country'),
                },
                'server': {
                    'name': results.get('server', {}).get('name'),
                    'country': results.get('server', {}).get('country'),
                    'sponsor': results.get('server', {}).get('sponsor'),
                },
            }

        except Exception as e:
            return {'error': f'Speed test failed: {str(e)}'}

    def test_dns(self, domain: str = "google.com") -> dict:
        """Test DNS resolution.

        Args:
            domain: Domain to resolve.

        Returns:
            Dict with DNS test results.
        """
        import socket

        print(f"{Colors.CYAN}[*] Testing DNS resolution...{Colors.RESET}")

        results = {
            'domain': domain,
            'resolved': False,
            'addresses': [],
        }

        try:
            start = time.time()
            addrs = socket.getaddrinfo(domain, 80)
            elapsed = round((time.time() - start) * 1000)

            results['resolved'] = True
            results['time_ms'] = elapsed
            results['addresses'] = list(set(addr[4][0] for addr in addrs))

            print(f"  {Colors.GREEN}[+]{Colors.RESET} Resolved {domain} in {elapsed}ms")
            for addr in results['addresses'][:3]:
                print(f"      {Colors.DIM}{addr}{Colors.RESET}")

        except socket.gaierror as e:
            results['error'] = f"DNS resolution failed: {e}"
            print(f"  {Colors.RED}[-]{Colors.RESET} DNS resolution failed")

        except Exception as e:
            results['error'] = str(e)
            print(f"  {Colors.RED}[-]{Colors.RESET} Error: {e}")

        return results

def color_speed(value: float, thresholds: tuple) -> str:
    """Color code speed values.

    Args:
        value: Speed value.
        thresholds: (low, medium) thresholds.

    Returns:
        Colored string.
    """
    low, medium = thresholds
    if value < low:
        return f"{Colors.RED}{value}{Colors.RESET}"
    elif value < medium:
        return f"{Colors.YELLOW}{value}{Colors.RESET}"
    else:
        return f"{Colors.GREEN}{value}{Colors.RESET}"

def display_speed_result(result: dict):
    """Display speed test results nicely."""
    if 'error' in result:
        print(f"\n{Colors.RED}[X] {result['error']}{Colors.RESET}")
        return

    print(f"\n{Colors.CYAN}{'=' * 50}{Colors.RESET}")
    print(f"{Colors.GREEN}{Colors.BOLD}  NETWORK SPEED TEST RESULTS{Colors.RESET}")
    print(f"{Colors.CYAN}{'=' * 50}{Colors.RESET}")

    # Download speed (low < 5 Mbps, medium < 25 Mbps)
    download = result['download_mbps']
    download_colored = color_speed(download, (5, 25))
    print(f"  {Colors.GREEN}Download:{Colors.RESET} {download_colored} Mbps")

    # Upload speed (low < 2 Mbps, medium < 10 Mbps)
    upload = result['upload_mbps']
    upload_colored = color_speed(upload, (2, 10))
    print(f"  {Colors.GREEN}Upload:{Colors.RESET}   {upload_colored} Mbps")

    # Ping (low > 100ms, medium > 50ms, inverted)
    ping = result['ping_ms']
    if ping > 100:
        ping_colored = f"{Colors.RED}{ping}{Colors.RESET}"
    elif ping > 50:
        ping_colored = f"{Colors.YELLOW}{ping}{Colors.RESET}"
    else:
        ping_colored = f"{Colors.GREEN}{ping}{Colors.RESET}"
    print(f"  {Colors.GREEN}Ping:{Colors.RESET}     {ping_colored} ms")

    # Client info
    client = result.get('client', {})
    if client:
        print(f"\n  {Colors.CYAN}Your Connection:{Colors.RESET}")
        if client.get('ip'):
            print(f"    IP: {client['ip']}")
        if client.get('isp'):
            print(f"    ISP: {client['isp']}")
        if client.get('country'):
            print(f"    Country: {client['country']}")

    # Server info
    server = result.get('server', {})
    if server:
        print(f"\n  {Colors.CYAN}Test Server:{Colors.RESET}")
        if server.get('sponsor'):
            print(f"    {server['sponsor']}")
        if server.get('name'):
            print(f"    {server['name']}, {server.get('country', '')}")

    print()

def display_menu():
    """Display the network test module menu."""
    speedtest_status = f"{Colors.GREEN}Available{Colors.RESET}" if HAS_SPEEDTEST else f"{Colors.RED}Not installed{Colors.RESET}"

    print(f"""
{Colors.CYAN}  Network Test{Colors.RESET}
{Colors.DIM}  Test network speed and connectivity{Colors.RESET}
{Colors.DIM}{'─' * 50}{Colors.RESET}

  {Colors.GREEN}[1]{Colors.RESET} Test Connectivity (ping websites)
  {Colors.GREEN}[2]{Colors.RESET} Full Speed Test [{speedtest_status}]
  {Colors.GREEN}[3]{Colors.RESET} Test DNS Resolution
  {Colors.GREEN}[4]{Colors.RESET} Run All Tests

  {Colors.RED}[0]{Colors.RESET} Back
""")

def run():
    """Main entry point for the module."""
    if not HAS_REQUESTS:
        print(f"{Colors.RED}[X] This module requires 'requests' library{Colors.RESET}")
        print(f"{Colors.DIM}    Install with: pip install requests{Colors.RESET}")
        input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
        return

    tester = NetworkTester()

    while True:
        display_menu()
        choice = input(f"{Colors.GREEN}Select option: {Colors.RESET}").strip()

        if choice == '0':
            break

        elif choice == '1':
            results = tester.test_connectivity()
            if 'error' not in results:
                total = results['success_count'] + results['fail_count']
                print(f"\n{Colors.GREEN}[+] Connectivity: {results['success_count']}/{total} tests passed{Colors.RESET}")
            input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        elif choice == '2':
            if not HAS_SPEEDTEST:
                print(f"\n{Colors.RED}[X] speedtest-cli library not installed{Colors.RESET}")
                print(f"{Colors.DIM}    Install with: pip install speedtest-cli{Colors.RESET}")
            else:
                results = tester.test_speed()
                display_speed_result(results)
            input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        elif choice == '3':
            print(f"\n{Colors.CYAN}Enter domain to resolve (default: google.com):{Colors.RESET}")
            domain = input(f"{Colors.GREEN}Domain: {Colors.RESET}").strip() or "google.com"
            tester.test_dns(domain)
            input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        elif choice == '4':
            print(f"\n{Colors.CYAN}{'=' * 50}{Colors.RESET}")
            print(f"{Colors.GREEN}{Colors.BOLD}  RUNNING ALL NETWORK TESTS{Colors.RESET}")
            print(f"{Colors.CYAN}{'=' * 50}{Colors.RESET}\n")

            # Connectivity
            print(f"{Colors.BOLD}1. Connectivity Test{Colors.RESET}")
            conn_results = tester.test_connectivity()

            # DNS
            print(f"\n{Colors.BOLD}2. DNS Resolution{Colors.RESET}")
            tester.test_dns()

            # Speed test
            if HAS_SPEEDTEST:
                print(f"\n{Colors.BOLD}3. Speed Test{Colors.RESET}")
                speed_results = tester.test_speed()
                display_speed_result(speed_results)
            else:
                print(f"\n{Colors.BOLD}3. Speed Test{Colors.RESET}")
                print(f"  {Colors.RED}[-]{Colors.RESET} Skipped (speedtest-cli not installed)")

            input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        else:
            print(f"{Colors.RED}[!] Invalid option{Colors.RESET}")

if __name__ == "__main__":
    run()