import os
import sys
import subprocess
import re
import time
import json
import platform
import getch
import shutil
import psutil
import requests
import zipfile
import io
import logging
from logging.handlers import TimedRotatingFileHandler
from concurrent.futures import ThreadPoolExecutor, as_completed
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, BarColumn, TimeElapsedColumn
from rich.align import Align
from rich.live import Live
from rich.text import Text
from rich import box
from rich.console import Group

console = Console()

# Ensure the program is run with elevated privileges on non-Windows systems.
# if os.getuid() != 0 and platform.system() != "Windows":
#     console.print("This program must be run as sudo.", style="red")
#     exit(1)

# ----------------- Configuration and Logging Setup -----------------
def get_config_path():
    """
    Returns the configuration file path and its directory.
    On Windows, it uses APPDATA; on Unix, a hidden folder in the home directory.
    """
    if platform.system() == "Windows":
        appdata = os.getenv("APPDATA")
        config_dir = os.path.join(appdata, "netvns")
    else:
        home = os.path.expanduser("~")
        config_dir = os.path.join(home, ".netvns")
    if not os.path.exists(config_dir):
        os.makedirs(config_dir)
    return os.path.join(config_dir, "config.json"), config_dir

CONFIG_PATH, CONFIG_DIR = get_config_path()
LOG_FILE = os.path.join(CONFIG_DIR, "app.log")

DEFAULT_CONFIG = {
    "dns_profiles": [
        {"keybind": "1", "name": "Google", "ipv4_primary": "8.8.8.8", "ipv4_secondary": "8.8.4.4",
         "ipv6_primary": "2001:4860:4860::8888", "ipv6_secondary": "2001:4860:4860::8844"},
        {"keybind": "2", "name": "Cloudflare", "ipv4_primary": "1.1.1.1", "ipv4_secondary": "1.0.0.1",
         "ipv6_primary": "2606:4700:4700::1111", "ipv6_secondary": "2606:4700:4700::1001"}
    ],
    "ping_sites": ["google.com"],
    "speedtest_cmd": {
        "Linux": "speedtest",
        "Darwin": "speedtest",
        "Windows": "speedtest.exe"
    },
    "logs": ""
}

# Initialize a global logger with a TimedRotatingFileHandler (7-day backup)
logger = logging.getLogger("netvns")
logger.setLevel(logging.DEBUG)
if not logger.handlers:
    handler = TimedRotatingFileHandler(LOG_FILE, when="D", interval=1, backupCount=7, encoding="utf-8")
    formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")
    handler.setFormatter(formatter)
    logger.addHandler(handler)

def load_config():
    """
    Loads configuration from the JSON file.
    If the file does not exist or fails to load, the default config is saved and returned.
    """
    if not os.path.isfile(CONFIG_PATH):
        save_config(DEFAULT_CONFIG)
        return DEFAULT_CONFIG
    try:
        with open(CONFIG_PATH, "r") as f:
            return json.load(f)
    except Exception as e:
        console.print(f"Failed to load config: {e}", style="red")
        return DEFAULT_CONFIG

def update_config_with_logs(config):
    """
    Reads the current log file content and stores it under the 'logs' key in the configuration.
    """
    if os.path.isfile(LOG_FILE):
        try:
            with open(LOG_FILE, "r", encoding="utf-8") as f:
                log_content = f.read()
            config["logs"] = log_content
        except Exception as e:
            logger.error(f"Failed to read log file: {e}", exc_info=True)
    return config

def save_config(config):
    """
    Saves the configuration (with updated log content) to the JSON file.
    """
    config = update_config_with_logs(config)
    try:
        with open(CONFIG_PATH, "w", encoding="utf-8") as f:
            json.dump(config, f, indent=4)
    except Exception as e:
        console.print(f"Failed to save config: {e}", style="red")
        logger.error(f"Failed to save config: {e}", exc_info=True)

config = load_config()

# ----------------- New Function: Retrieve Active IPv4 DNS -----------------
def get_active_ipv4_dns():
    """
    Retrieves the primary IPv4 DNS server address currently used by the active network connection.
    For Linux, it uses 'nmcli dev show <device>' and parses the 'IP4.DNS[1]:' line.
    Returns the DNS server as a string or None if not found.
    """
    system = platform.system()
    if system == "Linux":
        active = get_active_connection()  # For Linux, this returns a tuple (name, device)
        if active:
            name, device = active
            try:
                result = subprocess.run(f"nmcli dev show {device}", shell=True, capture_output=True, text=True, check=True)
                for line in result.stdout.splitlines():
                    if line.strip().startswith("IP4.DNS[1]:"):
                        parts = line.split(":", 1)
                        if len(parts) == 2:
                            return parts[1].strip()
            except subprocess.CalledProcessError as e:
                logger.error(f"Error retrieving active DNS using nmcli: {e}", exc_info=True)
    # For Windows or Darwin, similar logic can be added if needed.
    return None

# ----------------- Network and DNS Functions -----------------
def get_all_network_connections():
    """
    Retrieves all available network connections.
    Returns a list of tuples (connection_name, device) on Linux or a list of interface names on Windows.
    """
    connections = []
    system = platform.system()
    if system == "Linux":
        cmd = "nmcli -t -f NAME,DEVICE connection show"
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        for line in result.stdout.strip().splitlines():
            if ":" in line:
                name, device = line.split(":", 1)
                connections.append((name, device))
    elif system == "Windows":
        result = subprocess.run("netsh interface show interface", shell=True, capture_output=True, text=True)
        lines = result.stdout.splitlines()[3:]
        for line in lines:
            if "Connected" in line:
                parts = line.split()
                iface = " ".join(parts[3:])
                connections.append(iface)
    elif system == "Darwin":
        result = subprocess.run("networksetup -listallnetworkservices", shell=True, capture_output=True, text=True)
        for line in result.stdout.splitlines():
            if line.strip() and not line.lower().startswith("an asterisk") and "network services" not in line.lower():
                connections.append(line.strip())
    return connections

def get_active_connection():
    """
    Returns the active network connection.
    For Linux, it returns a tuple (connection_name, device); for others, returns the first connection found.
    """
    system = platform.system()
    conns = get_all_network_connections()
    return conns[0] if conns else None

def change_dns_for_all_connections(primary, alternative, ipv6_primary=None, ipv6_secondary=None, apply_all=True):
    """
    Changes the DNS settings for all or the current network connection.
    Sets both primary and secondary IPv4 DNS servers.
    Also sets IPv6 DNS if provided.
    """
    system = platform.system()
    if system == "Linux":
        connections = get_all_network_connections()
        if not apply_all:
            current = get_active_connection()
            if current:
                connections = [current]
            else:
                console.print("No active network connection found.", style="red")
                return
        for name, device in connections:
            if device:
                try:
                    # Clear existing DNS settings before applying new ones.
                    subprocess.run(f'nmcli con mod "{name}" ipv4.dns ""', shell=True, check=True)
                    # Set both primary and secondary IPv4 DNS servers.
                    subprocess.run(f'nmcli con mod "{name}" ipv4.dns "{primary} {alternative}" ipv4.ignore-auto-dns yes',
                                   shell=True, check=True)
                    if ipv6_primary and ipv6_secondary:
                        subprocess.run(
                            f'nmcli con mod "{name}" ipv6.dns "{ipv6_primary} {ipv6_secondary}" ipv6.ignore-auto-dns yes',
                            shell=True, check=True)
                    subprocess.run(f'nmcli con up "{name}" ifname "{device}"',
                                   shell=True, check=True)
                    console.print(f"DNS settings updated successfully for connection: {name}", style="green")
                    logger.info(f"Set DNS for connection {name}: primary={primary}, secondary={alternative}")
                except subprocess.CalledProcessError as e:
                    console.print(f"Failed to update DNS for connection: {name}", style="red")
                    console.print(str(e), style="red")
                    logger.error(f"Error setting DNS for {name}: {e}", exc_info=True)
    elif system == "Windows":
        interfaces = get_all_network_connections()
        if not apply_all:
            interfaces = [interfaces[0]] if interfaces else []
            if not interfaces:
                console.print("No active network connection found.", style="red")
                return
        for iface in interfaces:
            try:
                subprocess.run(f'netsh interface ip set dns name="{iface}" static {primary} primary',
                               shell=True, check=True)
                if alternative:
                    subprocess.run(f'netsh interface ip add dns name="{iface}" {alternative} index=2',
                                   shell=True, check=True)
                if ipv6_primary:
                    subprocess.run(f'netsh interface ipv6 set dns name="{iface}" static {ipv6_primary} primary',
                                   shell=True, check=True)
                if ipv6_secondary:
                    subprocess.run(f'netsh interface ipv6 add dns name="{iface}" {ipv6_secondary} index=2',
                                   shell=True, check=True)
                console.print(f"DNS settings updated successfully for interface: {iface}", style="green")
                logger.info(f"Set DNS for interface {iface}: primary={primary}, secondary={alternative}")
            except subprocess.CalledProcessError as e:
                console.print(f"Failed to update DNS for interface: {iface}", style="red")
                console.print(str(e), style="red")
                logger.error(f"Error setting DNS for interface {iface}: {e}", exc_info=True)
    elif system == "Darwin":
        services = get_all_network_connections()
        if not apply_all:
            services = [services[0]] if services else []
            if not services:
                console.print("No active network service found.", style="red")
                return
        for service in services:
            try:
                dns_servers = f"{primary} {alternative}".strip()
                subprocess.run(f'networksetup -setdnsservers "{service}" {dns_servers}',
                               shell=True, check=True)
                console.print(f"DNS settings updated successfully for service: {service}", style="green")
                logger.info(f"Set DNS for service {service}: primary={primary}, secondary={alternative}")
            except subprocess.CalledProcessError as e:
                console.print(f"Failed to update DNS for service: {service}", style="red")
                console.print(str(e), style="red")
                logger.error(f"Error setting DNS for service {service}: {e}", exc_info=True)

def reset_dns_for_all_connections(apply_all=True):
    """
    Resets DNS settings to automatic (DHCP) for all or the current network connection.
    """
    system = platform.system()
    if system == "Linux":
        connections = get_all_network_connections()
        if not apply_all:
            current = get_active_connection()
            connections = [current] if current else []
            if not connections:
                console.print("No active network connection found.", style="red")
                return
        for name, device in connections:
            if device:
                try:
                    subprocess.run(f'nmcli con mod "{name}" ipv4.ignore-auto-dns no',
                                   shell=True, check=True)
                    subprocess.run(f'nmcli con mod "{name}" ipv6.ignore-auto-dns no',
                                   shell=True, check=True)
                    subprocess.run(f'nmcli con up "{name}" ifname "{device}"',
                                   shell=True, check=True)
                    console.print(f"DNS settings reset successfully for connection: {name}", style="green")
                    logger.info(f"Reset DNS for connection {name}")
                except subprocess.CalledProcessError as e:
                    console.print(f"Failed to reset DNS for connection: {name}", style="red")
                    console.print(str(e), style="red")
                    logger.error(f"Error resetting DNS for {name}: {e}", exc_info=True)
    elif system == "Windows":
        interfaces = get_all_network_connections()
        if not apply_all:
            interfaces = [interfaces[0]] if interfaces else []
            if not interfaces:
                console.print("No active network connection found.", style="red")
                return
        for iface in interfaces:
            try:
                subprocess.run(f'netsh interface ip set dns name="{iface}" dhcp',
                               shell=True, check=True)
                subprocess.run(f'netsh interface ipv6 set dns name="{iface}" dhcp',
                               shell=True, check=True)
                console.print(f"DNS settings reset successfully for interface: {iface}", style="green")
                logger.info(f"Reset DNS for interface {iface}")
            except subprocess.CalledProcessError as e:
                console.print(f"Failed to reset DNS for interface: {iface}", style="red")
                console.print(str(e), style="red")
                logger.error(f"Error resetting DNS for interface {iface}: {e}", exc_info=True)
    elif system == "Darwin":
        services = get_all_network_connections()
        if not apply_all:
            services = [services[0]] if services else []
            if not services:
                console.print("No active network service found.", style="red")
                return
        for service in services:
            try:
                subprocess.run(f'networksetup -setdnsservers "{service}" Empty',
                               shell=True, check=True)
                console.print(f"DNS settings reset successfully for service: {service}", style="green")
                logger.info(f"Reset DNS for service {service}")
            except subprocess.CalledProcessError as e:
                console.print(f"Failed to reset DNS for service: {service}", style="red")
                console.print(str(e), style="red")
                logger.error(f"Error resetting DNS for service {service}: {e}", exc_info=True)

def flush_dns():
    """
    Flushes the DNS cache based on the underlying OS.
    """
    system = platform.system()
    try:
        if system == "Windows":
            subprocess.run('ipconfig /flushdns', shell=True, check=True)
        elif system == "Darwin":
            subprocess.run('sudo killall -HUP mDNSResponder', shell=True, check=True)
        else:
            if shutil.which("systemd-resolve"):
                subprocess.run('systemd-resolve --flush-caches', shell=True, check=True)
            else:
                subprocess.run('resolvectl flush-caches', shell=True, check=True)
        console.print("DNS cache flushed successfully.", style="green")
        logger.info("Flushed DNS cache")
    except subprocess.CalledProcessError as e:
        console.print("Failed to flush DNS cache.", style="red")
        console.print(str(e), style="red")
        logger.error(f"Error flushing DNS cache: {e}", exc_info=True)

def open_network_properties():
    """
    Opens the network properties/settings panel based on the OS.
    """
    if platform.system() == "Windows":
        subprocess.run("ncpa.cpl", shell=True)
    elif platform.system() == "Darwin":
        subprocess.run("open /System/Library/PreferencePanes/Network.prefPane", shell=True)
    else:
        subprocess.run('nm-connection-editor', shell=True)

# ----------------- SpeedTest Functions -----------------
def install_speedtest():
    """
    Installs the Speedtest CLI if not present.
    """
    os_name = platform.system()
    speedtest_cmd = config.get("speedtest_cmd", {}).get(os_name, "speedtest")
    if shutil.which(speedtest_cmd) is not None:
        return True
    console.print("Speedtest CLI not found. Installing...", style="yellow")
    try:
        if os_name == "Darwin":
            subprocess.run("brew tap teamookla/speedtest", shell=True, check=True)
            subprocess.run("brew update", shell=True, check=True)
            subprocess.run("brew install speedtest --force", shell=True, check=True)
        elif os_name == "Linux":
            if os.path.exists("/etc/debian_version"):
                subprocess.run("sudo apt-get install curl -y", shell=True, check=True)
                subprocess.run('curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash',
                               shell=True, check=True)
                subprocess.run("sudo apt-get install speedtest -y", shell=True, check=True)
            elif os.path.exists("/etc/redhat-release"):
                subprocess.run('curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash',
                               shell=True, check=True)
                subprocess.run("sudo yum install speedtest -y", shell=True, check=True)
            else:
                console.print("Unsupported Linux distribution for automatic speedtest installation.", style="red")
                return False
        elif os_name == "FreeBSD":
            subprocess.run("sudo pkg update && sudo pkg install -g libidn2 ca_root_nss", shell=True, check=True)
            subprocess.run('sudo pkg add "https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-freebsd12-x86_64.pkg"',
                           shell=True, check=True)
        elif os_name == "Windows":
            url = "https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-win64.zip"
            r = requests.get(url)
            z = zipfile.ZipFile(io.BytesIO(r.content))
            z.extractall()
        else:
            console.print("Unsupported OS for automatic speedtest installation.", style="red")
            return False
        console.print("Speedtest CLI installed successfully.", style="green")
        return True
    except subprocess.CalledProcessError as e:
        console.print(f"Failed to install Speedtest CLI: {e}", style="red")
        return False
    except Exception as e:
        console.print(f"Error during speedtest installation: {e}", style="red")
        return False

def run_speedtest():
    """
    Runs the Speedtest CLI and displays the result in a panel.
    """
    console.clear()
    os_name = platform.system()
    speedtest_cmd = config.get("speedtest_cmd", {}).get(os_name, "speedtest")
    if shutil.which(speedtest_cmd) is None:
        if not install_speedtest():
            console.print("Cannot run Speedtest CLI.", style="red")
            return
    with Progress(SpinnerColumn(), "[progress.description]{task.description}",
                  BarColumn(), TimeElapsedColumn(), transient=True) as progress:
        task = progress.add_task("Running speed test...", total=None)
        try:
            process = subprocess.Popen(speedtest_cmd, shell=True,
                                       stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            while process.poll() is None:
                time.sleep(0.1)
            stdout, stderr = process.communicate()
            progress.stop()
            result_panel = Panel(stdout, title="Speed Test Result", border_style="green", expand=True)
            console.print(result_panel)
        except Exception as e:
            error_panel = Panel(f"Failed to run speed test: {str(e)}", title="Error", border_style="red", expand=True)
            console.print(error_panel)
    time.sleep(1)

def ping_site(site):
    """
    Pings a given site twice and returns the average delay in ms.
    If both attempts fail, returns None as delay.
    """
    delays = []
    for _ in range(2):
        try:
            cmd = f"ping -c 1 -W 1 {site}" if platform.system() != "Windows" else f"ping -n 1 -w 1000 {site}"
            result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
            match = re.search(r'time=([\d.]+)', result.stdout)
            if match:
                delays.append(float(match.group(1)))
        except subprocess.CalledProcessError:
            pass
    if delays:
        avg_delay = sum(delays) / len(delays)
        return (site, avg_delay)
    else:
        return (site, None)

def ping_multiple_sites_with_progress(sites):
    """
    Continuously pings the list of sites and displays the results in real-time.
    Press Ctrl+C to exit.
    """
    console.clear()
    try:
        with Live(console=console, refresh_per_second=1) as live:
            while True:
                table = Table(title="Real-time Ping Results", box=box.ROUNDED, border_style="white", expand=True)
                table.add_column("Site", style="white", justify="center", no_wrap=True)
                table.add_column("Delay", style="green", justify="center", no_wrap=True)
                for site in config.get("ping_sites", []):
                    site_name, delay = ping_site(site)
                    if delay is not None:
                        table.add_row(site, f"{delay:.1f} ms")
                    else:
                        table.add_row(site, "[red]Failed[/red]")
                exit_text = Text("\nPress Ctrl+C to exit", style="yellow", justify="center")
                group = Group(table, Align.center(exit_text))
                live.update(group)
                time.sleep(1)
    except KeyboardInterrupt:
        pass

def real_time_ping():
    """
    Wrapper for continuous pinging of the configured sites.
    """
    ping_multiple_sites_with_progress(config.get("ping_sites", []))

def real_time_connectivity_monitor():
    """
    Monitors real-time network upload/download speeds and displays them.
    Converts speeds to KB/s or MB/s as appropriate.
    Press Ctrl+C to exit.
    """
    console.clear()
    prev = psutil.net_io_counters()
    try:
        with Live(console=console, refresh_per_second=1) as live:
            while True:
                time.sleep(1)
                current = psutil.net_io_counters()
                up_speed = current.bytes_sent - prev.bytes_sent
                down_speed = current.bytes_recv - prev.bytes_recv
                prev = current
                if up_speed/1024 < 1024:
                    up_display = f"{up_speed/1024:.1f} KB/s"
                else:
                    up_display = f"{up_speed/1024/1024:.1f} MB/s"
                if down_speed/1024 < 1024:
                    down_display = f"{down_speed/1024:.1f} KB/s"
                else:
                    down_display = f"{down_speed/1024/1024:.1f} MB/s"
                speed_text = Text(f"\n↑ Upload: {up_display}   ↓ Download: {down_display}\n", justify="center")
                panel = Panel(Align.center(speed_text), title="Real-time Connectivity", border_style="green")
                exit_text = Text("\nPress Ctrl+C to exit", style="yellow", justify="center")
                group = Group(panel, Align.center(exit_text))
                live.update(group)
    except KeyboardInterrupt:
        pass

# ----------------- DNS Profile Menu -----------------
def add_new_dns_profile():
    """
    Prompts the user to add a new DNS profile and saves it to the configuration.
    """
    console.clear()
    console.print("Add New DNS Profile", style="bold white")
    console.print("Enter keybind (single character): ", style="white", end="")
    keybind = console.input("").strip()
    console.print("Enter profile name: ", style="white", end="")
    name = console.input("").strip()
    console.print("Enter IPv4 primary: ", style="white", end="")
    ipv4_primary = console.input("").strip()
    console.print("Enter IPv4 secondary: ", style="white", end="")
    ipv4_secondary = console.input("").strip()
    console.print("Enter IPv6 primary (or leave blank): ", style="white", end="")
    ipv6_primary = console.input("").strip() or None
    console.print("Enter IPv6 secondary (or leave blank): ", style="white", end="")
    ipv6_secondary = console.input("").strip() or None

    new_profile = {
        "keybind": keybind,
        "name": name,
        "ipv4_primary": ipv4_primary,
        "ipv4_secondary": ipv4_secondary,
        "ipv6_primary": ipv6_primary,
        "ipv6_secondary": ipv6_secondary
    }
    config["dns_profiles"].append(new_profile)
    save_config(config)
    console.print("New DNS profile added.", style="green")
    time.sleep(1)

def delete_dns_profile():
    """
    Prompts the user to delete an existing DNS profile based on its keybind.
    """
    console.clear()
    console.print("Delete DNS Profile", style="bold white")
    console.print("Enter the keybind of the profile to delete: ", style="white", end="")
    key = console.input("").strip()
    profiles = config.get("dns_profiles", [])
    new_profiles = [p for p in profiles if p.get("keybind") != key]
    if len(new_profiles) != len(profiles):
        config["dns_profiles"] = new_profiles
        save_config(config)
        console.print("Profile deleted.", style="green")
    else:
        console.print("No matching profile found.", style="red")
    time.sleep(1)

def dns_profile_menu():
    """
    Displays the DNS Profile menu.
    Additionally, retrieves the active DNS configuration and compares the primary DNS
    with the list of DNS profiles. If a profile's primary DNS matches the active one, a tick (✓)
    is added to its display name.
    """
    colors = ["green", "yellow", "cyan", "magenta", "red", "bright_green", "bright_yellow", "bright_cyan", "bright_magenta", "white"]
    # Retrieve active primary DNS (if available)
    current_dns = get_active_ipv4_dns()
    while True:
        console.clear()
        table = Table(title="DNS Profile", box=box.ROUNDED, border_style="white", expand=True)
        table.add_column("Option", style="blue", justify="center")
        table.add_column("Profile", justify="left")
        table.add_column("Primary DNS", justify="left")
        table.add_column("Secondary DNS", justify="left")
        for idx, profile in enumerate(config.get("dns_profiles", [])):
            color = colors[idx % len(colors)]
            keybind = profile.get("keybind", "")
            name = profile.get("name", "")
            primary = profile.get("ipv4_primary", "")
            secondary = profile.get("ipv4_secondary", "")
            # If the active DNS matches the profile's primary DNS, add a tick mark.
            if current_dns and current_dns.strip() == primary.strip():
                name += " [✓]"
            option_text = Text(f"{keybind}.", style="blue")
            table.add_row(option_text, Text(name, style=color), Text(primary, style=color), Text(secondary, style=color))
        console.print(table)
        console.print("\nPress 'a' to Add | 'd' to Delete | 'r' to Reset DNS", style="white")
        console.print("Press the keybind of a profile to apply it, or '0' to go Back.", style="white")
        console.print("\nEnter your option: ", style="bold white", end="")
        choice = getch.getch()
        if not choice.strip():
            choice = "0"
        if choice == '0':
            break
        elif choice.lower() == 'r':
            console.print("\nResetting DNS settings...", style="white")
            console.print("Apply reset for (1) Current Network only or (2) All networks? ", style="white", end="")
            apply_choice = console.input("").strip()
            apply_all = True if apply_choice == "2" else False
            reset_dns_for_all_connections(apply_all)
        elif choice.lower() == 'a':
            add_new_dns_profile()
        elif choice.lower() == 'd':
            delete_dns_profile()
        else:
            selected = None
            for profile in config.get("dns_profiles", []):
                if profile.get("keybind") == choice:
                    selected = profile
                    break
            if selected:
                name = selected.get("name")
                primary = selected.get("ipv4_primary")
                secondary = selected.get("ipv4_secondary")
                ipv6_primary = selected.get("ipv6_primary")
                ipv6_secondary = selected.get("ipv6_secondary")
                console.print(f"\nChanging DNS to {name}", style="white")
                console.print("Apply changes for (1) Current Network only or (2) All networks? ", style="white", end="")
                apply_choice = console.input("").strip()
                apply_all = True if apply_choice == "2" else False
                change_dns_for_all_connections(primary, secondary, ipv6_primary, ipv6_secondary, apply_all)
            else:
                console.print("\nInvalid option, please try again.", style="red")
        time.sleep(1)

# ----------------- Ping Sites Editing -----------------
def edit_ping_sites():
    """
    Allows the user to add or remove sites from the ping list.
    """
    console.clear()
    console.print("Current Ping Sites:", style="bold white")
    for idx, site in enumerate(config.get("ping_sites", []), start=1):
        console.print(f"{idx}. {site}", style="white")
    console.print("\nEnter 'a' to Add, 'r' to Remove, or press Enter to go Back.", style="white", end="\n> ")
    choice = console.input("").strip().lower()
    if choice == "a":
        console.print("Enter comma-separated list of sites to add: ", style="white", end="")
        inp = console.input("").strip()
        new_sites = [s.strip() for s in inp.split(",") if s.strip()]
        for site in new_sites:
            if site not in config["ping_sites"]:
                config["ping_sites"].append(site)
        save_config(config)
        console.print("Ping sites updated.", style="green")
    elif choice == "r":
        console.print("Enter the site to remove: ", style="white", end="")
        site_to_remove = console.input("").strip()
        if site_to_remove in config["ping_sites"]:
            config["ping_sites"].remove(site_to_remove)
            save_config(config)
            console.print("Site removed.", style="green")
        else:
            console.print("Site not found.", style="red")
    time.sleep(1)

# ----------------- Advanced Settings Menu -----------------
def advanced_settings_menu():
    """
    Displays the Advanced Settings menu.
    """
    while True:
        console.clear()
        panel = Panel(Align.center(" "), title="Advanced Settings", border_style="white", expand=True)
        console.print(panel)
        console.print("\n1. Flush DNS", style="white")
        console.print("2. Open Network Properties", style="white")
        console.print("3. Show Config and Log Paths", style="white")
        console.print("\n0. Back", style="white")
        console.print("\nEnter your option: ", style="bold white", end="")
        choice = getch.getch()
        if not choice.strip():
            choice = "0"
        if choice == '0':
            break
        elif choice == '1':
            flush_dns()
        elif choice == '2':
            open_network_properties()
        elif choice == '3':
            console.print(f"\nConfig File Path: {CONFIG_PATH}", style="cyan")
            console.print(f"Log File Path: {LOG_FILE}", style="cyan")
            console.print("Press any key to continue...", style="white", end="")
            console.input("")
        else:
            console.print("Invalid option, please try again.", style="red")
        time.sleep(1)

# ----------------- Connectivity Test Menu -----------------
def connectivity_test_menu():
    """
    Displays the Connectivity Test menu with options for single ping,
    real-time ping, and connectivity monitoring.
    """
    while True:
        console.clear()
        panel = Panel(Align.center(" "), title="Connectivity Test", border_style="white", expand=True)
        console.print(panel)
        console.print("\n1. Ping a Single Site", style="white")
        console.print("2. Real-time Ping Multiple Sites", style="white")
        console.print("3. Real-time Connectivity Monitor\n", style="white")
        console.print("[bright_cyan]e[/bright_cyan]. Edit Site List", style="white")
        console.print("0. Back", style="white")
        console.print("\nEnter your option: ", style="bold white", end="")
        choice = getch.getch()
        if not choice.strip():
            choice = "0"
        if choice == '0':
            break
        elif choice == '1':
            console.print("\nEnter the site to ping: ", style="bold white", end="")
            site = console.input("")
            site, delay = ping_site(site)
            if delay is not None:
                result_panel = Panel(Align.center(f"\nPing delay: {delay:.1f} ms\n"),
                                       title=site, border_style="green", expand=True)
                console.print(result_panel)
            else:
                result_panel = Panel(Align.center("\nFailed\n"), title=site, border_style="red", expand=True)
                console.print(result_panel)
            time.sleep(1)
        elif choice == '2':
            console.print("\nPress Ctrl+C to stop real-time ping.", style="white")
            real_time_ping()
        elif choice == '3':
            console.print("\nPress Ctrl+C to stop the connectivity monitor.", style="white")
            real_time_connectivity_monitor()
        elif choice.lower() == 'e':
            edit_ping_sites()
        else:
            console.print("Invalid option, please try again.", style="red")
            time.sleep(1)

# ----------------- Main Menu -----------------
def main_menu():
    """
    Displays the main menu and routes the user to the selected module.
    """
    while True:
        # console.clear()
        # if os.geteuid() != 0:
        #     print("This program must be run as sudo, please enter your password:")
        # args = ['sudo', sys.executable] + sys.argv
        # os.execlpe('sudo', *args, dict(os.environ))
        
        console.clear()
        main_panel = Panel(Align.center(" "), title="NETVNS", border_style="white", expand=True)
        console.print(main_panel)
        console.print("\n1. DNS Profile", style="white")
        console.print("2. Advanced Settings", style="white")
        console.print("3. SpeedTest by Ookla", style="white")
        console.print("4. Connectivity Test", style="white")
        console.print("\n0. Exit", style="white")
        console.print("\nEnter your choice: ", style="bold white", end="")
        choice = getch.getch()
        if not choice.strip():
            choice = "0"
        if choice == '0':
            console.print("\nExiting program.", style="white")
            break
        elif choice == '1':
            dns_profile_menu()
        elif choice == '2':
            advanced_settings_menu()
        elif choice == '3':
            run_speedtest()
        elif choice == '4':
            connectivity_test_menu()
        else:
            console.print("Invalid option, please try again.", style="red")
            time.sleep(1)

if __name__ == "__main__":
    main_menu()
