Instruction: Create an AUTARCH module in the 'offense' category that metasploit framework interface. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: msf
Category: offense
Description: Metasploit Framework interface

Output:
"""
AUTARCH Metasploit Module
Enhanced interface for Metasploit Framework with module browser.

Provides easy access to MSF modules, exploits, and sessions.
Uses the centralized MSF interface from core/msf_interface.py.
Integrates with msf_terms.py and msf_modules.py for descriptions.
"""

import sys
import os
import re
import json
import time
import socket
from pathlib import Path
from typing import Dict, List, Optional, Any

# Module metadata
DESCRIPTION = "Metasploit Framework interface"
AUTHOR = "darkHal"
VERSION = "2.0"
CATEGORY = "offense"

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

from core.msf_interface import get_msf_interface
from core.banner import Colors, clear_screen, display_banner
from core.msf_terms import get_setting_info, get_setting_prompt, format_setting_help, validate_setting_value
from core.msf_modules import (
    get_module_info as get_library_module_info,
    get_module_description,
    search_modules as library_search_modules,
    get_modules_by_type,
    format_module_help,
    MSF_MODULES
)

class MSFMenu:
    """Enhanced Metasploit menu interface with module browser."""

    # Module categories for browsing
    MODULE_CATEGORIES = {
        'scanners': {
            'name': 'Scanners',
            'description': 'Network and vulnerability scanners',
            'types': ['auxiliary/scanner'],
            'color': Colors.CYAN
        },
        'exploits': {
            'name': 'Exploits',
            'description': 'Remote and local exploits',
            'types': ['exploit'],
            'color': Colors.RED
        },
        'post': {
            'name': 'Post-Exploitation',
            'description': 'Post-exploitation modules',
            'types': ['post'],
            'color': Colors.MAGENTA
        },
        'payloads': {
            'name': 'Payloads',
            'description': 'Payload generators',
            'types': ['payload'],
            'color': Colors.YELLOW
        },
        'auxiliary': {
            'name': 'Auxiliary',
            'description': 'Other auxiliary modules',
            'types': ['auxiliary'],
            'color': Colors.GREEN
        }
    }

    def __init__(self):
        self.msf = get_msf_interface()
        self.current_module = None
        self.current_module_type = None
        self.module_options = {}

        # Global target settings - persist across module selections
        self.global_settings = {
            'RHOSTS': '',
            'LHOST': '',
            'LPORT': '4444',
        }

    def print_status(self, message: str, status: str = "info"):
        """Print a status message."""
        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 wrap_text(self, text: str, width: int = 60, indent: str = "      ") -> str:
        """Word-wrap text with indent for subsequent lines."""
        words = text.split()
        lines = []
        current_line = ""

        for word in words:
            if len(current_line) + len(word) + 1 <= width:
                current_line += (" " if current_line else "") + word
            else:
                if current_line:
                    lines.append(current_line)
                current_line = word

        if current_line:
            lines.append(current_line)

        return f"\n{indent}".join(lines)

    def ensure_connected(self) -> bool:
        """Ensure connected to MSF RPC."""
        connected, msg = self.msf.ensure_connected()
        if not connected:
            print(f"\n{Colors.YELLOW}{msg}{Colors.RESET}")
            print(f"{Colors.DIM}Make sure msfrpcd is running: msfrpcd -P <password> -S{Colors.RESET}")
        return connected

    def resolve_hostname(self, hostname: str) -> Optional[str]:
        """Resolve hostname to IP address."""
        try:
            # Check if it's already an IP
            socket.inet_aton(hostname)
            return hostname
        except socket.error:
            pass

        # Try to resolve
        try:
            ip = socket.gethostbyname(hostname)
            return ip
        except socket.gaierror:
            return None

    def get_local_ip(self) -> str:
        """Get local IP address for LHOST."""
        try:
            # Connect to external address to get local IP
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            return local_ip
        except Exception:
            return "127.0.0.1"

    # =========================================================================
    # MAIN MENU
    # =========================================================================

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

        print(f"{Colors.RED}{Colors.BOLD}  Metasploit Framework{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")

        # Connection status
        if self.msf.is_connected:
            print(f"  {Colors.GREEN}Status: Connected{Colors.RESET}")
        else:
            print(f"  {Colors.YELLOW}Status: Disconnected{Colors.RESET}")

        # Show current global settings
        if any(self.global_settings.values()):
            print()
            if self.global_settings['RHOSTS']:
                print(f"  {Colors.CYAN}Target:{Colors.RESET} {self.global_settings['RHOSTS']}")
            if self.global_settings['LHOST']:
                print(f"  {Colors.CYAN}LHOST:{Colors.RESET}  {self.global_settings['LHOST']}")
            if self.global_settings['LPORT'] and self.global_settings['LPORT'] != '4444':
                print(f"  {Colors.CYAN}LPORT:{Colors.RESET}  {self.global_settings['LPORT']}")

        # Current module
        if self.current_module:
            print(f"  {Colors.YELLOW}Module:{Colors.RESET} {self.current_module_type}/{self.current_module}")

        print()
        print(f"  {Colors.RED}[1]{Colors.RESET} Set Target        {Colors.DIM}- Configure target & listener settings{Colors.RESET}")
        print(f"  {Colors.RED}[2]{Colors.RESET} Module Browser    {Colors.DIM}- Browse modules by category{Colors.RESET}")
        print(f"  {Colors.RED}[3]{Colors.RESET} Search Modules    {Colors.DIM}- Search all modules{Colors.RESET}")
        print()
        print(f"  {Colors.RED}[4]{Colors.RESET} Current Module    {Colors.DIM}- View/configure selected module{Colors.RESET}")
        print(f"  {Colors.RED}[5]{Colors.RESET} Run Module        {Colors.DIM}- Execute current module{Colors.RESET}")
        print()
        print(f"  {Colors.RED}[6]{Colors.RESET} Sessions          {Colors.DIM}- View and interact with sessions{Colors.RESET}")
        print(f"  {Colors.RED}[7]{Colors.RESET} Jobs              {Colors.DIM}- View running background jobs{Colors.RESET}")
        print()
        print(f"  {Colors.RED}[8]{Colors.RESET} MSF Console       {Colors.DIM}- Direct console access{Colors.RESET}")
        print(f"  {Colors.RED}[9]{Colors.RESET} Quick Scan        {Colors.DIM}- Common scanners{Colors.RESET}")
        print(f"  {Colors.RED}[E]{Colors.RESET} Exploit Suggester {Colors.DIM}- Suggest exploits from vuln data{Colors.RESET}")
        print()
        print(f"  {Colors.DIM}[0]{Colors.RESET} Back to Main Menu")
        print()

    # =========================================================================
    # GLOBAL TARGET SETTINGS
    # =========================================================================

    def show_target_settings(self):
        """Configure global target settings."""
        while True:
            clear_screen()
            display_banner()

            print(f"{Colors.RED}{Colors.BOLD}  Target Configuration{Colors.RESET}")
            print(f"{Colors.DIM}  Set target and listener options before selecting modules{Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            # Display current settings with term bank descriptions
            rhosts_info = get_setting_info('RHOSTS')
            lhost_info = get_setting_info('LHOST')
            lport_info = get_setting_info('LPORT')

            rhosts_val = self.global_settings['RHOSTS'] or f"{Colors.YELLOW}(not set){Colors.RESET}"
            lhost_val = self.global_settings['LHOST'] or f"{Colors.YELLOW}(not set){Colors.RESET}"
            lport_val = self.global_settings['LPORT'] or '4444'

            print(f"  {Colors.RED}[1]{Colors.RESET} RHOSTS  = {rhosts_val}")
            print(f"      {Colors.DIM}{self.wrap_text(rhosts_info['description'])}{Colors.RESET}")
            print()
            print(f"  {Colors.RED}[2]{Colors.RESET} LHOST   = {lhost_val}")
            print(f"      {Colors.DIM}{self.wrap_text(lhost_info['description'])}{Colors.RESET}")
            print()
            print(f"  {Colors.RED}[3]{Colors.RESET} LPORT   = {lport_val}")
            print(f"      {Colors.DIM}{self.wrap_text(lport_info['description'])}{Colors.RESET}")
            print()
            print(f"  {Colors.GREEN}[A]{Colors.RESET} Auto-detect LHOST")
            print(f"  {Colors.GREEN}[R]{Colors.RESET} Resolve hostname to IP")
            print()
            print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
            print()

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

                if choice == '0' or not choice:
                    break
                elif choice == '1':
                    self._set_rhosts()
                elif choice == '2':
                    self._set_lhost()
                elif choice == '3':
                    self._set_lport()
                elif choice == 'a':
                    self._auto_detect_lhost()
                elif choice == 'r':
                    self._resolve_hostname()

            except (EOFError, KeyboardInterrupt):
                break

    def _set_rhosts(self):
        """Set RHOSTS with validation and domain resolution."""
        print()
        print(format_setting_help('RHOSTS'))
        print()

        current = self.global_settings['RHOSTS']
        prompt = f"Target [{current}]: " if current else "Target: "
        value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()

        if not value and current:
            return  # Keep current

        if value:
            # Check if it's a hostname that needs resolution
            if not any(c.isdigit() for c in value.split('/')[0].split('-')[0]):
                # Looks like a hostname
                print(f"{Colors.CYAN}[*] Resolving {value}...{Colors.RESET}")
                ip = self.resolve_hostname(value)
                if ip:
                    print(f"{Colors.GREEN}[+] Resolved to {ip}{Colors.RESET}")
                    use_ip = input(f"{Colors.WHITE}Use resolved IP? (y/n) [{Colors.GREEN}y{Colors.WHITE}]: {Colors.RESET}").strip().lower()
                    if use_ip != 'n':
                        value = ip
                else:
                    print(f"{Colors.YELLOW}[!] Could not resolve hostname{Colors.RESET}")

            # Validate
            valid, msg = validate_setting_value('RHOSTS', value)
            if valid:
                self.global_settings['RHOSTS'] = value
                self.print_status(f"RHOSTS => {value}", "success")
            else:
                self.print_status(msg, "warning")
                self.global_settings['RHOSTS'] = value  # Set anyway, user might know better

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

    def _set_lhost(self):
        """Set LHOST."""
        print()
        print(format_setting_help('LHOST'))
        print()

        current = self.global_settings['LHOST']
        auto_ip = self.get_local_ip()

        print(f"  {Colors.DIM}Detected local IP: {auto_ip}{Colors.RESET}")
        prompt = f"LHOST [{current or auto_ip}]: "
        value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()

        if not value:
            value = current or auto_ip

        if value:
            self.global_settings['LHOST'] = value
            self.print_status(f"LHOST => {value}", "success")

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

    def _set_lport(self):
        """Set LPORT."""
        print()
        print(format_setting_help('LPORT'))
        print()

        current = self.global_settings['LPORT'] or '4444'
        prompt = f"LPORT [{current}]: "
        value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()

        if not value:
            value = current

        # Validate port
        try:
            port = int(value)
            if 1 <= port <= 65535:
                self.global_settings['LPORT'] = value
                self.print_status(f"LPORT => {value}", "success")
            else:
                self.print_status("Port must be between 1 and 65535", "warning")
        except ValueError:
            self.print_status("Invalid port number", "warning")

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

    def _auto_detect_lhost(self):
        """Auto-detect LHOST."""
        ip = self.get_local_ip()
        self.global_settings['LHOST'] = ip
        self.print_status(f"LHOST => {ip} (auto-detected)", "success")
        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _resolve_hostname(self):
        """Resolve a hostname to IP."""
        print()
        hostname = input(f"{Colors.WHITE}Hostname to resolve: {Colors.RESET}").strip()

        if hostname:
            print(f"{Colors.CYAN}[*] Resolving {hostname}...{Colors.RESET}")
            ip = self.resolve_hostname(hostname)
            if ip:
                print(f"{Colors.GREEN}[+] {hostname} => {ip}{Colors.RESET}")
                use_as_target = input(f"{Colors.WHITE}Use as RHOSTS? (y/n): {Colors.RESET}").strip().lower()
                if use_as_target == 'y':
                    self.global_settings['RHOSTS'] = ip
                    self.print_status(f"RHOSTS => {ip}", "success")
            else:
                self.print_status(f"Could not resolve {hostname}", "error")

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

    # =========================================================================
    # MODULE BROWSER
    # =========================================================================

    def show_module_browser(self):
        """Browse modules by category."""
        while True:
            clear_screen()
            display_banner()

            print(f"{Colors.RED}{Colors.BOLD}  Module Browser{Colors.RESET}")
            print(f"{Colors.DIM}  Browse Metasploit modules by category{Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            # Show categories
            for i, (cat_id, cat_info) in enumerate(self.MODULE_CATEGORIES.items(), 1):
                color = cat_info['color']
                print(f"  {color}[{i}]{Colors.RESET} {cat_info['name']}")
                print(f"      {Colors.DIM}{cat_info['description']}{Colors.RESET}")

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

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

                if choice == '0' or not choice:
                    break

                try:
                    idx = int(choice) - 1
                    cat_ids = list(self.MODULE_CATEGORIES.keys())
                    if 0 <= idx < len(cat_ids):
                        self._browse_category(cat_ids[idx])
                except ValueError:
                    pass

            except (EOFError, KeyboardInterrupt):
                break

    def _browse_category(self, category: str):
        """Browse modules in a category with pagination."""
        cat_info = self.MODULE_CATEGORIES.get(category)
        if not cat_info:
            return

        # Get modules from library that match this category
        modules = []
        for path, info in MSF_MODULES.items():
            for type_prefix in cat_info['types']:
                if path.startswith(type_prefix):
                    modules.append({'path': path, **info})
                    break

        # Also try to get from MSF if connected
        if self.msf.is_connected:
            for type_prefix in cat_info['types']:
                if '/' in type_prefix:
                    # e.g., auxiliary/scanner
                    mtype = type_prefix.split('/')[0]
                else:
                    mtype = type_prefix

                msf_modules = self.msf.list_modules(mtype)
                if msf_modules:
                    for mod_path in msf_modules[:50]:  # Limit to avoid overwhelming
                        if mod_path not in [m['path'] for m in modules]:
                            # Add basic info for modules not in library
                            modules.append({
                                'path': mod_path,
                                'name': mod_path.split('/')[-1].replace('_', ' ').title(),
                                'description': 'Module from Metasploit (use "info" for details)',
                                'tags': []
                            })

        if not modules:
            self.print_status(f"No modules found in {cat_info['name']}", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Pagination
        page_size = 20
        page = 0
        total_pages = (len(modules) + page_size - 1) // page_size

        while True:
            clear_screen()
            display_banner()

            print(f"{cat_info['color']}{Colors.BOLD}  {cat_info['name']}{Colors.RESET}")
            print(f"{Colors.DIM}  Page {page + 1} of {total_pages} ({len(modules)} modules){Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            # Display modules in 2 columns
            start_idx = page * page_size
            end_idx = min(start_idx + page_size, len(modules))
            page_modules = modules[start_idx:end_idx]

            # Split into two columns
            half = (len(page_modules) + 1) // 2
            col1 = page_modules[:half]
            col2 = page_modules[half:]

            for i in range(max(len(col1), len(col2))):
                line = ""

                # Column 1
                if i < len(col1):
                    num = start_idx + i + 1
                    mod = col1[i]
                    name = mod.get('name', mod['path'].split('/')[-1])
                    if len(name) > 22:
                        name = name[:19] + "..."
                    line += f"  {cat_info['color']}[{num:2}]{Colors.RESET} {name:22}"
                else:
                    line += " " * 30

                # Column 2
                if i < len(col2):
                    num = start_idx + half + i + 1
                    mod = col2[i]
                    name = mod.get('name', mod['path'].split('/')[-1])
                    if len(name) > 22:
                        name = name[:19] + "..."
                    line += f"  {cat_info['color']}[{num:2}]{Colors.RESET} {name:22}"

                print(line)

            print()
            print(f"  {Colors.DIM}[N]{Colors.RESET} Next page   {Colors.DIM}[P]{Colors.RESET} Previous   {Colors.DIM}[0]{Colors.RESET} Back")
            print()

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

                if choice == '0' or not choice:
                    break
                elif choice == 'n' and page < total_pages - 1:
                    page += 1
                elif choice == 'p' and page > 0:
                    page -= 1
                else:
                    try:
                        idx = int(choice) - 1
                        if 0 <= idx < len(modules):
                            self._show_module_details(modules[idx])
                    except ValueError:
                        pass

            except (EOFError, KeyboardInterrupt):
                break

    def _show_module_details(self, module_info: Dict):
        """Show module details and offer to use it."""
        clear_screen()
        display_banner()

        path = module_info['path']
        name = module_info.get('name', path.split('/')[-1])

        print(f"{Colors.RED}{Colors.BOLD}  {name}{Colors.RESET}")
        print(f"{Colors.DIM}  {path}{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        # Try to get full help from library
        help_text = format_module_help(path)
        if help_text:
            print(help_text)
        else:
            # Fall back to basic info
            desc = module_info.get('description', 'No description available')
            print(f"  {Colors.CYAN}Description:{Colors.RESET}")
            # Word wrap description
            words = desc.split()
            line = "    "
            for word in words:
                if len(line) + len(word) > 70:
                    print(line)
                    line = "    "
                line += word + " "
            if line.strip():
                print(line)
            print()

            if 'author' in module_info:
                authors = module_info['author']
                if isinstance(authors, list):
                    authors = ', '.join(authors)
                print(f"  {Colors.CYAN}Author:{Colors.RESET} {authors}")

            if 'cve' in module_info and module_info['cve']:
                print(f"  {Colors.CYAN}CVE:{Colors.RESET} {module_info['cve']}")

            if 'reliability' in module_info:
                print(f"  {Colors.CYAN}Reliability:{Colors.RESET} {module_info['reliability']}")

            if 'notes' in module_info:
                print()
                print(f"  {Colors.YELLOW}Notes:{Colors.RESET} {module_info['notes']}")

        print()
        print(f"  {Colors.GREEN}[U]{Colors.RESET} Use this module")
        print(f"  {Colors.CYAN}[I]{Colors.RESET} Get info from MSF")
        print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
        print()

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

            if choice == 'u':
                self._select_module(path)
            elif choice == 'i':
                self._show_msf_info(path)

        except (EOFError, KeyboardInterrupt):
            pass

    def _select_module(self, module_path: str):
        """Select a module and prepare it for execution."""
        if not self.ensure_connected():
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Parse module type and name
        parts = module_path.split('/', 1)
        mtype = parts[0]
        mname = parts[1] if len(parts) > 1 else module_path

        self.print_status(f"Loading {module_path}...", "info")

        # Get module info and options from MSF
        info = self.msf.get_module_info(module_path)
        options = self.msf.get_module_options(module_path)

        if not options:
            self.print_status(f"Failed to load module: {self.msf.last_error}", "error")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        self.current_module = mname
        self.current_module_type = mtype
        self.module_options = {}

        # Set defaults from module
        for opt_name, opt_info in options.items():
            if isinstance(opt_info, dict):
                default = opt_info.get('default')
                if default is not None and default != '':
                    self.module_options[opt_name] = default

        # Apply global settings
        if self.global_settings['RHOSTS'] and 'RHOSTS' in options:
            self.module_options['RHOSTS'] = self.global_settings['RHOSTS']
        if self.global_settings['RHOSTS'] and 'RHOST' in options:
            self.module_options['RHOST'] = self.global_settings['RHOSTS']
        if self.global_settings['LHOST'] and 'LHOST' in options:
            self.module_options['LHOST'] = self.global_settings['LHOST']
        if self.global_settings['LPORT'] and 'LPORT' in options:
            self.module_options['LPORT'] = self.global_settings['LPORT']

        self.print_status(f"Module loaded: {mtype}/{mname}", "success")

        # Show what was auto-filled
        auto_filled = []
        if 'RHOSTS' in self.module_options or 'RHOST' in self.module_options:
            target = self.module_options.get('RHOSTS') or self.module_options.get('RHOST')
            if target:
                auto_filled.append(f"Target: {target}")
        if 'LHOST' in self.module_options and self.module_options['LHOST']:
            auto_filled.append(f"LHOST: {self.module_options['LHOST']}")

        if auto_filled:
            print(f"{Colors.DIM}  Auto-filled: {', '.join(auto_filled)}{Colors.RESET}")

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

    def _show_msf_info(self, module_path: str):
        """Get and display module info from MSF."""
        if not self.ensure_connected():
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        self.print_status(f"Fetching info for {module_path}...", "info")

        info = self.msf.get_module_info(module_path)
        if not info:
            self.print_status(f"Failed to get info: {self.msf.last_error}", "error")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        clear_screen()
        display_banner()

        print(f"{Colors.RED}{Colors.BOLD}  Module Info (from MSF){Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        # Display info fields
        fields = ['name', 'fullname', 'description', 'author', 'references', 'rank', 'platform', 'arch']
        for field in fields:
            if field in info and info[field]:
                value = info[field]
                if isinstance(value, list):
                    if field == 'references':
                        print(f"  {Colors.CYAN}{field}:{Colors.RESET}")
                        for ref in value[:5]:
                            if isinstance(ref, (list, tuple)) and len(ref) >= 2:
                                print(f"    - {ref[0]}: {ref[1]}")
                            else:
                                print(f"    - {ref}")
                    else:
                        value = ', '.join(str(v) for v in value[:5])
                        print(f"  {Colors.CYAN}{field}:{Colors.RESET} {value}")
                elif field == 'description':
                    print(f"  {Colors.CYAN}{field}:{Colors.RESET}")
                    # Word wrap
                    words = str(value).split()
                    line = "    "
                    for word in words:
                        if len(line) + len(word) > 70:
                            print(line)
                            line = "    "
                        line += word + " "
                    if line.strip():
                        print(line)
                else:
                    print(f"  {Colors.CYAN}{field}:{Colors.RESET} {value}")

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

    # =========================================================================
    # SEARCH MODULES
    # =========================================================================

    def search_modules(self):
        """Search for MSF modules."""
        if not self.ensure_connected():
            return

        print(f"\n{Colors.BOLD}Search Metasploit Modules{Colors.RESET}")
        print(f"{Colors.DIM}Examples: 'smb', 'apache', 'ssh', 'cve:2021', 'eternalblue'{Colors.RESET}\n")

        query = input(f"{Colors.WHITE}Search: {Colors.RESET}").strip()
        if not query:
            return

        self.print_status(f"Searching for '{query}'...", "info")

        # Search both library and MSF
        library_results = library_search_modules(query)
        msf_results = self.msf.search_modules(query)

        # Combine results, preferring library entries
        combined = {}
        for mod in library_results:
            combined[mod['path']] = mod

        if msf_results:
            for mod in msf_results:
                if isinstance(mod, dict):
                    fullname = mod.get('fullname', '')
                    if fullname and fullname not in combined:
                        combined[fullname] = {
                            'path': fullname,
                            'name': mod.get('name', fullname.split('/')[-1]),
                            'description': 'Module from Metasploit',
                            'rank': mod.get('rank', '')
                        }
                elif isinstance(mod, str) and mod not in combined:
                    combined[mod] = {
                        'path': mod,
                        'name': mod.split('/')[-1],
                        'description': 'Module from Metasploit'
                    }

        results = list(combined.values())

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

        # Display with pagination
        page_size = 15
        page = 0
        total_pages = (len(results) + page_size - 1) // page_size

        while True:
            clear_screen()
            display_banner()

            print(f"{Colors.GREEN}{Colors.BOLD}  Search Results: '{query}'{Colors.RESET}")
            print(f"{Colors.DIM}  Page {page + 1} of {total_pages} ({len(results)} found){Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            start_idx = page * page_size
            end_idx = min(start_idx + page_size, len(results))
            page_results = results[start_idx:end_idx]

            for i, mod in enumerate(page_results, start_idx + 1):
                name = mod.get('name', mod['path'].split('/')[-1])
                path = mod['path']
                if len(name) > 30:
                    name = name[:27] + "..."
                print(f"  {Colors.RED}[{i:2}]{Colors.RESET} {name}")
                print(f"       {Colors.DIM}{path}{Colors.RESET}")

            print()
            print(f"  {Colors.DIM}[N]{Colors.RESET} Next   {Colors.DIM}[P]{Colors.RESET} Previous   {Colors.DIM}[0]{Colors.RESET} Back")
            print()

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

                if choice == '0' or not choice:
                    break
                elif choice == 'n' and page < total_pages - 1:
                    page += 1
                elif choice == 'p' and page > 0:
                    page -= 1
                else:
                    try:
                        idx = int(choice) - 1
                        if 0 <= idx < len(results):
                            self._show_module_details(results[idx])
                    except ValueError:
                        pass

            except (EOFError, KeyboardInterrupt):
                break

    # =========================================================================
    # CURRENT MODULE MANAGEMENT
    # =========================================================================

    def show_current_module(self):
        """Show and configure current module options."""
        if not self.current_module:
            self.print_status("No module selected. Use Module Browser or Search first.", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        if not self.ensure_connected():
            return

        full_path = f"{self.current_module_type}/{self.current_module}"
        options = self.msf.get_module_options(full_path)

        if not options:
            self.print_status(f"Failed to get options: {self.msf.last_error}", "error")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        while True:
            clear_screen()
            display_banner()

            print(f"{Colors.RED}{Colors.BOLD}  {full_path}{Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            # Separate required and optional
            required = []
            optional = []

            for name, info in options.items():
                if isinstance(info, dict):
                    is_required = info.get('required', False)
                    current_val = self.module_options.get(name, info.get('default', ''))
                    desc = info.get('desc', '')[:35]

                    entry = (name, current_val, desc, is_required)
                    if is_required:
                        required.append(entry)
                    else:
                        optional.append(entry)

            # Show required first
            if required:
                print(f"  {Colors.RED}Required Options:{Colors.RESET}")
                for i, (name, val, desc, _) in enumerate(required, 1):
                    val_display = str(val) if val else f"{Colors.YELLOW}(not set){Colors.RESET}"
                    print(f"    {Colors.CYAN}[{i}]{Colors.RESET} {name:18} = {val_display}")

                    # Get help from term bank
                    term_info = get_setting_info(name)
                    if term_info:
                        print(f"        {Colors.DIM}{self.wrap_text(term_info['description'], width=55, indent='        ')}{Colors.RESET}")
                    else:
                        print(f"        {Colors.DIM}{self.wrap_text(desc, width=55, indent='        ')}{Colors.RESET}")
                print()

            # Show optional (just first 8)
            if optional:
                print(f"  {Colors.DIM}Optional (first 8):{Colors.RESET}")
                for name, val, desc, _ in optional[:8]:
                    val_display = str(val)[:20] if val else ""
                    print(f"    {Colors.DIM}{name:18} = {val_display}{Colors.RESET}")
                print()

            print(f"  {Colors.GREEN}[S]{Colors.RESET} Set option")
            print(f"  {Colors.GREEN}[R]{Colors.RESET} Run module")
            print(f"  {Colors.CYAN}[A]{Colors.RESET} Show all options")
            print()
            print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
            print()

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

                if choice == '0' or not choice:
                    break
                elif choice == 's':
                    self.set_option()
                elif choice == 'r':
                    self.run_module()
                    break
                elif choice == 'a':
                    self._show_all_options(options)
                elif choice.isdigit():
                    idx = int(choice) - 1
                    if 0 <= idx < len(required):
                        self._set_specific_option(required[idx][0], options)

            except (EOFError, KeyboardInterrupt):
                break

    def _show_all_options(self, options: Dict):
        """Show all module options."""
        clear_screen()
        display_banner()

        full_path = f"{self.current_module_type}/{self.current_module}"
        print(f"{Colors.BOLD}All Options for {full_path}{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")

        for name, info in sorted(options.items()):
            if isinstance(info, dict):
                is_required = info.get('required', False)
                current_val = self.module_options.get(name, info.get('default', ''))
                desc = info.get('desc', '')

                req_marker = f"{Colors.RED}*{Colors.RESET}" if is_required else " "
                val_display = str(current_val)[:30] if current_val else f"{Colors.DIM}(empty){Colors.RESET}"

                print(f"  {req_marker} {Colors.CYAN}{name:20}{Colors.RESET} = {val_display}")
                if desc:
                    print(f"      {Colors.DIM}{self.wrap_text(desc, width=55, indent='      ')}{Colors.RESET}")

        print(f"\n{Colors.DIM}* = required{Colors.RESET}")
        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _set_specific_option(self, opt_name: str, options: Dict):
        """Set a specific option with term bank help."""
        print()

        # Show help from term bank or module
        term_info = get_setting_info(opt_name)
        if term_info:
            print(format_setting_help(opt_name))
        elif opt_name in options:
            opt_info = options[opt_name]
            desc = opt_info.get('desc', 'No description')
            print(f"  {Colors.CYAN}{opt_name}:{Colors.RESET} {desc}")
        print()

        current = self.module_options.get(opt_name, '')
        prompt = f"{opt_name} [{current}]: " if current else f"{opt_name}: "
        value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()

        if value or not current:
            self.module_options[opt_name] = value
            self.print_status(f"{opt_name} => {value}", "success")
        else:
            self.print_status(f"{opt_name} unchanged", "info")

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

    def set_option(self):
        """Set a module option."""
        if not self.current_module:
            self.print_status("No module selected.", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        print(f"\n{Colors.BOLD}Set Option{Colors.RESET}")
        print(f"{Colors.DIM}Common: RHOSTS, RPORT, LHOST, LPORT, PAYLOAD{Colors.RESET}\n")

        opt_name = input(f"{Colors.WHITE}Option name: {Colors.RESET}").strip().upper()
        if not opt_name:
            return

        # Show help
        term_info = get_setting_info(opt_name)
        if term_info:
            print()
            print(format_setting_help(opt_name))
            print()

        current = self.module_options.get(opt_name, '')
        prompt = f"{Colors.WHITE}Value [{current}]: {Colors.RESET}" if current else f"{Colors.WHITE}Value: {Colors.RESET}"
        opt_value = input(prompt).strip()

        if opt_value or not current:
            self.module_options[opt_name] = opt_value
            self.print_status(f"{opt_name} => {opt_value}", "success")
        else:
            self.print_status(f"{opt_name} unchanged", "info")

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

    def run_module(self):
        """Execute the current module."""
        if not self.current_module:
            self.print_status("No module selected.", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        if not self.ensure_connected():
            return

        full_path = f"{self.current_module_type}/{self.current_module}"

        print(f"\n{Colors.BOLD}Run Module: {full_path}{Colors.RESET}")
        print(f"\n{Colors.CYAN}Options:{Colors.RESET}")
        for k, v in self.module_options.items():
            if v:
                print(f"  {k} = {v}")

        confirm = input(f"\n{Colors.YELLOW}Execute? (y/n): {Colors.RESET}").strip().lower()
        if confirm != 'y':
            return

        self.print_status("Executing module...", "info")

        # Use job execution for exploits, console execution for auxiliary/scanners
        if self.current_module_type in ['exploit', 'post']:
            success, job_id, error = self.msf.execute_module_job(full_path, self.module_options)
            if success:
                self.print_status(f"Module running as Job {job_id}", "success")
            else:
                self.print_status(f"Execution failed: {error}", "error")
        else:
            result = self.msf.run_module(full_path, self.module_options, timeout=120)
            self.msf.print_result(result, verbose=True)

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

    # =========================================================================
    # SESSIONS AND JOBS
    # =========================================================================

    def show_sessions(self):
        """Show active sessions."""
        if not self.ensure_connected():
            return

        sessions = self.msf.list_sessions()

        print(f"\n{Colors.BOLD}Active Sessions{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")

        if not sessions:
            self.print_status("No active sessions", "info")
        else:
            for sid, info in sessions.items():
                if isinstance(info, dict):
                    stype = info.get('type', 'shell')
                    target = info.get('target_host', 'unknown')
                    user = info.get('username', '')
                    via = info.get('via_exploit', '')[:30]
                    print(f"  {Colors.GREEN}[{sid}]{Colors.RESET} {stype} @ {target}")
                    if user:
                        print(f"      {Colors.DIM}User: {user}{Colors.RESET}")
                    if via:
                        print(f"      {Colors.DIM}Via: {via}{Colors.RESET}")
                else:
                    print(f"  {Colors.GREEN}[{sid}]{Colors.RESET} {info}")

            print()
            sid = input(f"{Colors.WHITE}Interact with session (or Enter to skip): {Colors.RESET}").strip()
            if sid and sid in sessions:
                self.interact_session(sid)

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

    def interact_session(self, session_id: str):
        """Interact with a session."""
        print(f"\n{Colors.GREEN}Interacting with session {session_id}{Colors.RESET}")
        print(f"{Colors.DIM}Type 'exit' to return to menu{Colors.RESET}\n")

        while True:
            try:
                cmd = input(f"{Colors.RED}session({session_id})>{Colors.RESET} ").strip()

                if cmd.lower() == 'exit':
                    break

                if not cmd:
                    continue

                self.msf.session_write(session_id, cmd)
                time.sleep(1)
                success, output = self.msf.session_read(session_id)
                if success and output:
                    print(output)

            except (EOFError, KeyboardInterrupt):
                print()
                break
            except Exception as e:
                self.print_status(f"Session error: {e}", "error")
                break

    def show_jobs(self):
        """Show running jobs."""
        if not self.ensure_connected():
            return

        jobs = self.msf.list_jobs()

        print(f"\n{Colors.BOLD}Running Jobs{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")

        if not jobs:
            self.print_status("No running jobs", "info")
        else:
            for jid, jname in jobs.items():
                print(f"  {Colors.YELLOW}[{jid}]{Colors.RESET} {jname}")

            print()
            jid = input(f"{Colors.WHITE}Kill job (or Enter to skip): {Colors.RESET}").strip()
            if jid and jid in jobs:
                if self.msf.stop_job(jid):
                    self.print_status(f"Job {jid} stopped", "success")
                else:
                    self.print_status(f"Failed to stop job {jid}", "error")

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

    # =========================================================================
    # CONSOLE AND QUICK SCAN
    # =========================================================================

    def console_command(self):
        """Run console commands directly."""
        if not self.ensure_connected():
            return

        print(f"\n{Colors.BOLD}MSF Console{Colors.RESET}")
        print(f"{Colors.DIM}Enter commands directly (type 'exit' to return){Colors.RESET}\n")

        while True:
            try:
                cmd = input(f"{Colors.RED}msf>{Colors.RESET} ").strip()

                if cmd.lower() == 'exit':
                    break

                if not cmd:
                    continue

                success, output = self.msf.run_console_command(cmd)
                if output:
                    print(output)

            except (EOFError, KeyboardInterrupt):
                print()
                break

    def quick_scan(self):
        """Quick scanner with pre-set target."""
        if not self.ensure_connected():
            return

        clear_screen()
        display_banner()

        print(f"{Colors.RED}{Colors.BOLD}  Quick Scan{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")

        # Show current target
        if self.global_settings['RHOSTS']:
            print(f"  {Colors.GREEN}Target: {self.global_settings['RHOSTS']}{Colors.RESET}")
        else:
            print(f"  {Colors.YELLOW}Target: Not set (will prompt){Colors.RESET}")
        print()

        scanners = [
            ("auxiliary/scanner/portscan/tcp", "TCP Port Scanner", "Scan for open TCP ports"),
            ("auxiliary/scanner/smb/smb_version", "SMB Version", "Identify Windows/SMB version"),
            ("auxiliary/scanner/smb/smb_ms17_010", "MS17-010 Check", "Check for EternalBlue vulnerability"),
            ("auxiliary/scanner/ssh/ssh_version", "SSH Version", "Identify SSH server version"),
            ("auxiliary/scanner/http/http_version", "HTTP Version", "Identify web server version"),
            ("auxiliary/scanner/ftp/ftp_version", "FTP Version", "Identify FTP server version"),
        ]

        for i, (mod, name, desc) in enumerate(scanners, 1):
            print(f"  {Colors.RED}[{i}]{Colors.RESET} {name}")
            print(f"      {Colors.DIM}{desc}{Colors.RESET}")

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

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

        try:
            idx = int(choice) - 1
            if 0 <= idx < len(scanners):
                scanner_mod, scanner_name, _ = scanners[idx]

                # Get target
                target = self.global_settings['RHOSTS']
                if not target:
                    target = input(f"{Colors.WHITE}Target (IP/range): {Colors.RESET}").strip()

                if not target:
                    return

                options = {'RHOSTS': target, 'THREADS': '10'}

                print(f"\n{Colors.CYAN}Running {scanner_name} against {target}...{Colors.RESET}")

                result = self.msf.run_scanner(scanner_mod, target, options=options, timeout=120)
                self.msf.print_result(result, verbose=False)

        except (ValueError, IndexError):
            pass
        except Exception as e:
            self.print_status(f"Scanner failed: {e}", "error")

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

    # =========================================================================
    # EXPLOIT SUGGESTER
    # =========================================================================

    def exploit_suggester(self):
        """Suggest exploits based on vulnerability scan results."""
        clear_screen()
        display_banner()

        print(f"{Colors.RED}{Colors.BOLD}  Exploit Suggester{Colors.RESET}")
        print(f"{Colors.DIM}  Suggest attack paths based on detected vulnerabilities{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        print(f"  {Colors.RED}[1]{Colors.RESET} Load vuln_correlator JSON")
        print(f"  {Colors.RED}[2]{Colors.RESET} Run fresh scan")
        print(f"  {Colors.RED}[3]{Colors.RESET} Manual service list")
        print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
        print()

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

        services = []
        cves = []

        if choice == "1":
            # Load from vuln correlator JSON
            results_dir = Path("results")
            json_files = sorted(results_dir.glob("vuln_correlator_*.json")) if results_dir.exists() else []
            if not json_files:
                self.print_status("No vuln correlator results found. Run OSINT > Vulnerability Correlator first.", "warning")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
                return

            print(f"\n{Colors.CYAN}Available vuln reports:{Colors.RESET}")
            for i, f in enumerate(json_files, 1):
                print(f"  {Colors.RED}[{i}]{Colors.RESET} {f.name}")

            sel = input(f"\n{Colors.WHITE}Select: {Colors.RESET}").strip()
            try:
                idx = int(sel) - 1
                with open(json_files[idx], 'r') as f:
                    data = json.load(f)

                for corr in data.get('correlations', []):
                    svc = corr.get('service', {})
                    services.append(svc)
                    for cve in corr.get('cves', []):
                        cves.append(cve)
            except (ValueError, IndexError, json.JSONDecodeError) as e:
                self.print_status(f"Error loading file: {e}", "error")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
                return

        elif choice == "2":
            target = self.global_settings.get('RHOSTS', '') or input(f"{Colors.WHITE}Target: {Colors.RESET}").strip()
            if not target:
                return

            self.print_status(f"Running nmap -sV on {target}...", "info")
            import subprocess
            try:
                result = subprocess.run(f"nmap -sV -T4 {target}", shell=True, capture_output=True, text=True, timeout=300)
                if result.returncode == 0:
                    port_re = re.compile(r'(\d+)/(tcp|udp)\s+open\s+(\S+)\s*(.*)')
                    for line in result.stdout.split('\n'):
                        m = port_re.match(line.strip())
                        if m:
                            parts = m.group(4).strip().split()
                            services.append({
                                'port': int(m.group(1)),
                                'service': parts[0] if parts else m.group(3),
                                'version': parts[1] if len(parts) > 1 else '',
                                'host': target,
                            })
            except Exception as e:
                self.print_status(f"Scan failed: {e}", "error")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
                return

        elif choice == "3":
            print(f"\n{Colors.DIM}Enter services (format: port service version), empty line to finish:{Colors.RESET}")
            while True:
                line = input(f"{Colors.WHITE}  > {Colors.RESET}").strip()
                if not line:
                    break
                parts = line.split()
                if len(parts) >= 2:
                    services.append({
                        'port': int(parts[0]) if parts[0].isdigit() else 0,
                        'service': parts[1],
                        'version': parts[2] if len(parts) > 2 else '',
                    })
        else:
            return

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

        # Try LLM-based suggestion first
        suggestions = []
        try:
            from core.llm import get_llm
            llm = get_llm()
            if llm.is_loaded:
                self.print_status("Using LLM for exploit analysis...", "info")
                prompt = self._build_exploit_prompt(services, cves)

                print(f"\n{Colors.CYAN}{'─' * 60}{Colors.RESET}")
                print(f"{Colors.BOLD}Exploit Analysis{Colors.RESET}")
                print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}\n")

                # Stream response
                full_response = ""
                for token in llm.generate(prompt, stream=True, max_tokens=1024):
                    print(token, end='', flush=True)
                    full_response += token
                print()

                suggestions = self._parse_exploit_suggestions(full_response)
            else:
                raise Exception("LLM not loaded")
        except Exception:
            # Fallback: direct CVE-to-MSF mapping
            self.print_status("Using direct CVE-to-MSF mapping (no LLM)...", "info")
            suggestions = self._fallback_exploit_suggestions(services, cves)

        # Display suggestions
        if suggestions:
            print(f"\n{Colors.CYAN}{'─' * 60}{Colors.RESET}")
            print(f"{Colors.BOLD}Suggested Exploits{Colors.RESET}")
            print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}\n")

            for i, s in enumerate(suggestions, 1):
                print(f"  {Colors.RED}[{i}]{Colors.RESET} {s.get('module', 'N/A')}")
                print(f"      Target: {s.get('target', 'N/A')}")
                if s.get('cve'):
                    print(f"      CVE: {s['cve']}")
                if s.get('reasoning'):
                    print(f"      {Colors.DIM}{s['reasoning']}{Colors.RESET}")
                print()

            self._offer_autoload(suggestions)
        else:
            self.print_status("No matching exploits found in module library", "info")

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

    def _build_exploit_prompt(self, services: list, cves: list) -> str:
        """Build LLM prompt for exploit suggestion."""
        # Gather available MSF modules
        relevant_modules = []
        for svc in services:
            svc_name = svc.get('service', '').lower()
            results = library_search_modules(svc_name, max_results=5)
            for mod in results:
                if mod['path'] not in [m['path'] for m in relevant_modules]:
                    relevant_modules.append(mod)

        # Also search by CVE
        for cve in cves[:10]:
            cve_id = cve.get('cve_id', '')
            if cve_id:
                results = library_search_modules(cve_id, max_results=3)
                for mod in results:
                    if mod['path'] not in [m['path'] for m in relevant_modules]:
                        relevant_modules.append(mod)

        prompt = "You are a penetration testing assistant. Based on the following target information, suggest the top 5 attack paths.\n\n"
        prompt += "TARGET SERVICES:\n"
        for svc in services:
            prompt += f"  - Port {svc.get('port', '?')}: {svc.get('service', '?')} {svc.get('version', '')}\n"

        if cves:
            prompt += "\nKNOWN VULNERABILITIES:\n"
            for cve in cves[:15]:
                prompt += f"  - {cve.get('cve_id', '?')} ({cve.get('severity', '?')} {cve.get('cvss_score', '?')}): {(cve.get('description', '') or '')[:100]}\n"

        if relevant_modules:
            prompt += "\nAVAILABLE METASPLOIT MODULES:\n"
            for mod in relevant_modules[:15]:
                prompt += f"  - {mod['path']}: {mod.get('name', '')}\n"

        prompt += "\nFor each suggestion, provide: module path, target service, CVE (if applicable), and reasoning.\n"
        prompt += "Format each as: RANK. MODULE_PATH | TARGET | CVE | REASONING\n"

        return prompt

    def _parse_exploit_suggestions(self, response: str) -> list:
        """Parse exploit suggestions from LLM response."""
        suggestions = []
        lines = response.split('\n')

        for line in lines:
            line = line.strip()
            if not line:
                continue

            # Try to parse "N. module | target | cve | reasoning" format
            if '|' in line and ('/' in line or 'exploit' in line.lower() or 'auxiliary' in line.lower()):
                parts = [p.strip() for p in line.split('|')]
                # Remove leading number
                first = re.sub(r'^\d+\.\s*', '', parts[0])

                suggestion = {
                    'module': first,
                    'target': parts[1] if len(parts) > 1 else '',
                    'cve': parts[2] if len(parts) > 2 else '',
                    'reasoning': parts[3] if len(parts) > 3 else '',
                }
                suggestions.append(suggestion)

        return suggestions[:5]

    def _fallback_exploit_suggestions(self, services: list, cves: list) -> list:
        """Fallback exploit suggestion using direct CVE-to-MSF mapping."""
        suggestions = []
        seen_modules = set()

        # Search by CVE
        for cve in cves[:20]:
            cve_id = cve.get('cve_id', '')
            if not cve_id:
                continue
            results = library_search_modules(cve_id, max_results=3)
            for mod in results:
                if mod['path'] not in seen_modules:
                    seen_modules.add(mod['path'])
                    suggestions.append({
                        'module': mod['path'],
                        'target': mod.get('name', ''),
                        'cve': cve_id,
                        'reasoning': f"CVSS {cve.get('cvss_score', '?')} - Direct CVE match",
                    })

        # Search by service name
        for svc in services:
            svc_name = svc.get('service', '').lower()
            if not svc_name:
                continue
            results = library_search_modules(svc_name, max_results=3)
            for mod in results:
                if mod['path'] not in seen_modules and mod['path'].startswith('exploit'):
                    seen_modules.add(mod['path'])
                    suggestions.append({
                        'module': mod['path'],
                        'target': f"{svc_name} on port {svc.get('port', '?')}",
                        'cve': ', '.join(mod.get('cve', []) or []),
                        'reasoning': f"Service match: {svc_name} {svc.get('version', '')}",
                    })

        return suggestions[:5]

    def _offer_autoload(self, suggestions: list):
        """Offer to auto-load a suggested module."""
        choice = input(f"\n{Colors.WHITE}Load a module? (enter number or 0 to skip): {Colors.RESET}").strip()
        if not choice or choice == '0':
            return

        try:
            idx = int(choice) - 1
            if 0 <= idx < len(suggestions):
                module_path = suggestions[idx].get('module', '')
                if module_path and '/' in module_path:
                    self.print_status(f"Loading {module_path}...", "info")
                    self._select_module(module_path)
        except (ValueError, IndexError):
            pass

    # =========================================================================
    # MAIN LOOP
    # =========================================================================

    def run(self):
        """Main loop."""
        while True:
            self.show_main_menu()

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

                if choice == "0":
                    break
                elif choice == "1":
                    self.show_target_settings()
                elif choice == "2":
                    self.show_module_browser()
                elif choice == "3":
                    self.search_modules()
                elif choice == "4":
                    self.show_current_module()
                elif choice == "5":
                    self.run_module()
                elif choice == "6":
                    self.show_sessions()
                elif choice == "7":
                    self.show_jobs()
                elif choice == "8":
                    self.console_command()
                elif choice == "9":
                    self.quick_scan()
                elif choice.lower() == "e":
                    self.exploit_suggester()

            except (EOFError, KeyboardInterrupt):
                print()
                break

def run():
    """Module entry point."""
    menu = MSFMenu()
    menu.run()

if __name__ == "__main__":
    run()