#!/usr/bin/python3
# +--------------------------------------------------------------------+
# EFA-NG Firewall & Access Control Manager (Native NFTables & GeoIP)
# Version 20260901
# Compatible with CentOS Stream 10 / RHEL / Debian / Ubuntu
# +--------------------------------------------------------------------+
# Copyright (C) 2026  https://efa-ng.space.ua
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# +--------------------------------------------------------------------+

import sys
import os
import json
import subprocess
import ipaddress
import argparse
import urllib.request
import time
import shutil

# Try importing rich, provide clear fallback if missing
try:
    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, Confirm
    from rich.columns import Columns
    from rich import box
    from rich.align import Align
    from rich.rule import Rule
    RICH_AVAILABLE = True
except ImportError:
    RICH_AVAILABLE = False

CONFIG_PATH = "/etc/eFa/firewall.conf"
DEFAULT_CONFIG_PATH = "/etc/eFa/firewall.conf.default"
NFT_EFA_FILE = "/etc/nftables/efa.nft"
GEOIP_CACHE_DIR = "/var/eFa/geoip"

console = Console() if RICH_AVAILABLE else None

COUNTRY_NAMES = {
    "AD": "Andorra", "AE": "United Arab Emirates", "AF": "Afghanistan", "AG": "Antigua and Barbuda",
    "AL": "Albania", "AM": "Armenia", "AO": "Angola", "AR": "Argentina", "AT": "Austria", "AU": "Australia",
    "AZ": "Azerbaijan", "BA": "Bosnia and Herzegovina", "BB": "Barbados", "BD": "Bangladesh", "BE": "Belgium",
    "BF": "Burkina Faso", "BG": "Bulgaria", "BH": "Bahrain", "BI": "Burundi", "BJ": "Benin", "BM": "Bermuda",
    "BN": "Brunei", "BO": "Bolivia", "BR": "Brazil", "BS": "Bahamas", "BT": "Bhutan", "BW": "Botswana",
    "BY": "Belarus", "BZ": "Belize", "CA": "Canada", "CD": "Congo (DRC)", "CF": "Central African Republic",
    "CG": "Congo", "CH": "Switzerland", "CI": "Ivory Coast", "CL": "Chile", "CM": "Cameroon", "CN": "China",
    "CO": "Colombia", "CR": "Costa Rica", "CU": "Cuba", "CV": "Cape Verde", "CY": "Cyprus", "CZ": "Czech Republic",
    "DE": "Germany", "DJ": "Djibouti", "DK": "Denmark", "DM": "Dominica", "DO": "Dominican Republic",
    "DZ": "Algeria", "EC": "Ecuador", "EE": "Estonia", "EG": "Egypt", "ES": "Spain", "ET": "Ethiopia",
    "FI": "Finland", "FJ": "Fiji", "FR": "France", "GA": "Gabon", "GB": "United Kingdom", "GE": "Georgia",
    "GH": "Ghana", "GM": "Gambia", "GN": "Guinea", "GR": "Greece", "GT": "Guatemala", "HK": "Hong Kong",
    "HN": "Honduras", "HR": "Croatia", "HT": "Haiti", "HU": "Hungary", "ID": "Indonesia", "IE": "Ireland",
    "IL": "Israel", "IN": "India", "IQ": "Iraq", "IR": "Iran", "IS": "Iceland", "IT": "Italy", "JM": "Jamaica",
    "JO": "Jordan", "JP": "Japan", "KE": "Kenya", "KG": "Kyrgyzstan", "KH": "Cambodia", "KR": "South Korea",
    "KW": "Kuwait", "KZ": "Kazakhstan", "LA": "Laos", "LB": "Lebanon", "LI": "Liechtenstein", "LK": "Sri Lanka",
    "LT": "Lithuania", "LU": "Luxembourg", "LV": "Latvia", "LY": "Libya", "MA": "Morocco", "MC": "Monaco",
    "MD": "Moldova", "ME": "Montenegro", "MG": "Madagascar", "MK": "North Macedonia", "ML": "Mali",
    "MM": "Myanmar", "MN": "Mongolia", "MO": "Macau", "MT": "Malta", "MU": "Mauritius", "MV": "Maldives",
    "MX": "Mexico", "MY": "Malaysia", "MZ": "Mozambique", "NA": "Namibia", "NG": "Nigeria", "NI": "Nicaragua",
    "NL": "Netherlands", "NO": "Norway", "NP": "Nepal", "NZ": "New Zealand", "OM": "Oman", "PA": "Panama",
    "PE": "Peru", "PH": "Philippines", "PK": "Pakistan", "PL": "Poland", "PT": "Portugal", "PY": "Paraguay",
    "QA": "Qatar", "RO": "Romania", "RS": "Serbia", "RU": "Russian Federation", "RW": "Rwanda",
    "SA": "Saudi Arabia", "SC": "Seychelles", "SD": "Sudan", "SE": "Sweden", "SG": "Singapore",
    "SI": "Slovenia", "SK": "Slovakia", "SN": "Senegal", "SO": "Somalia", "SY": "Syria", "TH": "Thailand",
    "TJ": "Tajikistan", "TM": "Turkmenistan", "TN": "Tunisia", "TR": "Turkey", "TW": "Taiwan",
    "TZ": "Tanzania", "UA": "Ukraine", "UG": "Uganda", "US": "United States", "UY": "Uruguay",
    "UZ": "Uzbekistan", "VE": "Venezuela", "VN": "Vietnam", "YE": "Yemen", "ZA": "South Africa",
    "ZM": "Zambia", "ZW": "Zimbabwe"
}

def get_country_name(code):
    return COUNTRY_NAMES.get(code.upper(), code.upper())

def get_default_config():
    return {
        "version": "1.0",
        "enabled": True,
        "allow_ping": True,
        "public_ports": [
            {"port": "25", "proto": "tcp", "comment": "SMTP Mail Server (Public)"}
        ],
        "admins": [
            {"ip": "127.0.0.1", "comment": "Localhost loopback"},
            {"ip": "195.230.150.128/27", "comment": "Admin Subnet (NOC)"},
            {"ip": "212.1.66.158", "comment": "Admin External Host"},
            {"ip": "195.230.150.69", "comment": "EFA-NG Dev Server"}
        ],
        "trusted": {
            "ports": [
                {"port": "3306", "proto": "tcp", "comment": "MariaDB Database"},
                {"port": "10050", "proto": "tcp", "comment": "Zabbix Agent"}
            ],
            "entries": [
                {"ip": "195.230.150.0/24", "comment": "Infocom Trusted Network (DB & Zabbix)"}
            ]
        },
        "http_only": {
            "ports": [
                {"port": "80", "proto": "tcp", "comment": "HTTP Web Interface"},
                {"port": "443", "proto": "tcp", "comment": "HTTPS Secure Web Interface"}
            ],
            "entries": [
                {"ip": "195.230.150.128/27", "comment": "NOC Subnet"},
                {"ip": "212.1.66.0/24", "comment": "Infocom Network"},
                {"ip": "94.101.42.162", "comment": "Remote Web User 1"},
                {"ip": "185.239.185.200", "comment": "Remote Web User 2"},
                {"ip": "77.235.167.209", "comment": "Remote Web User 3"},
                {"ip": "195.230.150.225", "comment": "Remote Web User 4"},
                {"ip": "195.230.150.69", "comment": "Dev Server Web Access"}
            ]
        },
        "mail_clients": {
            "ports": [
                {"port": "110", "proto": "tcp", "comment": "POP3 Mail Access"},
                {"port": "143", "proto": "tcp", "comment": "IMAP Mail Access"},
                {"port": "587", "proto": "tcp", "comment": "SMTP Submission"},
                {"port": "993", "proto": "tcp", "comment": "IMAPS Secure IMAP"},
                {"port": "995", "proto": "tcp", "comment": "POP3S Secure POP3"}
            ],
            "entries": [
                {"ip": "195.230.150.128/27", "comment": "NOC Subnet"},
                {"ip": "212.1.66.0/24", "comment": "Infocom Network"},
                {"ip": "94.101.42.162", "comment": "Remote Mail Client 1"},
                {"ip": "185.239.185.200", "comment": "Remote Mail Client 2"},
                {"ip": "77.235.167.209", "comment": "Remote Mail Client 3"},
                {"ip": "195.230.150.225", "comment": "Remote Mail Client 4"},
                {"ip": "195.230.150.69", "comment": "Dev Server Mail Access"}
            ]
        },
        "geoip": {
            "enabled": True,
            "global_blacklist": [
                {"country": "RU", "comment": "Russian Federation (Global Drop)"},
                {"country": "CN", "comment": "China (Global Drop)"},
                {"country": "BR", "comment": "Brazil (Global Drop)"}
            ],
            "groups": {
                "http_only": {
                    "mode": "whitelist",
                    "countries": [
                        {"country": "UA", "comment": "Ukraine"},
                        {"country": "US", "comment": "United States"},
                        {"country": "DE", "comment": "Germany"}
                    ]
                },
                "mail_clients": {
                    "mode": "disabled",
                    "countries": []
                },
                "public_ports": {
                    "mode": "disabled",
                    "countries": []
                },
                "trusted": {
                    "mode": "disabled",
                    "countries": []
                }
            }
        }
    }

def load_config():
    if os.path.exists(CONFIG_PATH):
        try:
            with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                cfg = json.load(f)
                dirty = False
                if "mail_clients" not in cfg:
                    cfg["mail_clients"] = {
                        "ports": [
                            {"port": "110", "proto": "tcp", "comment": "POP3 Mail Access"},
                            {"port": "143", "proto": "tcp", "comment": "IMAP Mail Access"},
                            {"port": "587", "proto": "tcp", "comment": "SMTP Submission"},
                            {"port": "993", "proto": "tcp", "comment": "IMAPS Secure IMAP"},
                            {"port": "995", "proto": "tcp", "comment": "POP3S Secure POP3"}
                        ],
                        "entries": []
                    }
                    dirty = True
                if "geoip" not in cfg:
                    cfg["geoip"] = {
                        "enabled": True,
                        "global_blacklist": [],
                        "groups": {
                            "http_only": {"mode": "disabled", "countries": []},
                            "mail_clients": {"mode": "disabled", "countries": []},
                            "public_ports": {"mode": "disabled", "countries": []},
                            "trusted": {"mode": "disabled", "countries": []}
                        }
                    }
                    dirty = True
                if dirty:
                    save_config(cfg)
                return cfg
        except Exception as e:
            if RICH_AVAILABLE:
                console.print(f"[bold red]ERROR:[/] Failed to parse {CONFIG_PATH}: {e}")
            else:
                print(f"[ERROR] Failed to parse {CONFIG_PATH}: {e}")
            sys.exit(1)
    elif os.path.exists(DEFAULT_CONFIG_PATH):
        try:
            with open(DEFAULT_CONFIG_PATH, "r", encoding="utf-8") as f:
                cfg = json.load(f)
                save_config(cfg)
                return cfg
        except Exception:
            pass
    cfg = get_default_config()
    save_config(cfg)
    return cfg

def save_config(cfg):
    os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
    tmp_path = CONFIG_PATH + ".tmp"
    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(cfg, f, indent=2, ensure_ascii=False)
        f.write("\n")
    os.replace(tmp_path, CONFIG_PATH)
    try:
        os.chmod(CONFIG_PATH, 0o640)
    except Exception:
        pass

def normalize_ip(ip_str):
    ip_str = ip_str.strip()
    try:
        net = ipaddress.ip_network(ip_str, strict=False)
        if net.num_addresses == 1:
            return str(net.network_address), net.version
        return str(net), net.version
    except ValueError:
        return None, None

# ----------------- GeoIP Fetch & Cache Engine -----------------
def fetch_country_zone(country_code, force=False):
    os.makedirs(GEOIP_CACHE_DIR, exist_ok=True)
    cc = country_code.strip().lower()
    f_v4 = os.path.join(GEOIP_CACHE_DIR, f"{cc}.zone")
    f_v6 = os.path.join(GEOIP_CACHE_DIR, f"{cc}6.zone")

    need_v4 = force or not os.path.exists(f_v4) or (time.time() - os.path.getmtime(f_v4) > 14 * 86400)
    need_v6 = force or not os.path.exists(f_v6) or (time.time() - os.path.getmtime(f_v6) > 14 * 86400)

    if need_v4:
        v4_lines = []
        try:
            url = f"http://www.ipdeny.com/ipblocks/data/countries/{cc}.zone"
            req = urllib.request.Request(url, headers={"User-Agent": "eFa-Firewall/6.0"})
            with urllib.request.urlopen(req, timeout=8) as resp:
                for line in resp.read().decode("utf-8", errors="ignore").splitlines():
                    line = line.strip()
                    if line and not line.startswith("#"):
                        v4_lines.append(line)
        except Exception:
            pass

        if not v4_lines:
            try:
                url = f"https://raw.githubusercontent.com/ipverse/rir-ip/master/country/{cc}/ipv4-aggregated.txt"
                req = urllib.request.Request(url, headers={"User-Agent": "eFa-Firewall/6.0"})
                with urllib.request.urlopen(req, timeout=8) as resp:
                    for line in resp.read().decode("utf-8", errors="ignore").splitlines():
                        line = line.strip()
                        if line and not line.startswith("#"):
                            v4_lines.append(line)
            except Exception:
                pass

        if v4_lines:
            with open(f_v4, "w", encoding="utf-8") as f:
                f.write("\n".join(v4_lines) + "\n")

    if need_v6:
        v6_lines = []
        try:
            url = f"https://raw.githubusercontent.com/ipverse/rir-ip/master/country/{cc}/ipv6-aggregated.txt"
            req = urllib.request.Request(url, headers={"User-Agent": "eFa-Firewall/6.0"})
            with urllib.request.urlopen(req, timeout=8) as resp:
                for line in resp.read().decode("utf-8", errors="ignore").splitlines():
                    line = line.strip()
                    if line and not line.startswith("#"):
                        v6_lines.append(line)
        except Exception:
            pass

        if v6_lines:
            with open(f_v6, "w", encoding="utf-8") as f:
                f.write("\n".join(v6_lines) + "\n")

def get_country_cidrs(country_code, auto_fetch=True):
    cc = country_code.strip().lower()
    f_v4 = os.path.join(GEOIP_CACHE_DIR, f"{cc}.zone")
    f_v6 = os.path.join(GEOIP_CACHE_DIR, f"{cc}6.zone")

    if auto_fetch and (not os.path.exists(f_v4) or not os.path.exists(f_v6)):
        fetch_country_zone(cc)

    v4_cidrs = set()
    v6_cidrs = set()

    if os.path.exists(f_v4):
        with open(f_v4, "r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#"):
                    v4_cidrs.add(line)

    if os.path.exists(f_v6):
        with open(f_v6, "r", encoding="utf-8", errors="ignore") as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#"):
                    v6_cidrs.add(line)

    return v4_cidrs, v6_cidrs

def update_all_geoip_caches(cfg, silent=False):
    if not silent:
        if RICH_AVAILABLE:
            console.print("[bold cyan]● Updating GeoIP country CIDR zone caches...[/]")
        else:
            print("[eFa-Firewall] Updating GeoIP country CIDR zone caches...")

    countries = set()
    for item in cfg.get("geoip", {}).get("global_blacklist", []):
        c = item.get("country", "").strip().upper()
        if c:
            countries.add(c)

    for gname, gdata in cfg.get("geoip", {}).get("groups", {}).items():
        for item in gdata.get("countries", []):
            c = item.get("country", "").strip().upper()
            if c:
                countries.add(c)

    if not countries:
        if not silent:
            if RICH_AVAILABLE:
                console.print("[yellow]● No GeoIP countries configured.[/]")
            else:
                print("No GeoIP countries configured.")
        return

    for c in sorted(countries):
        if not silent and RICH_AVAILABLE:
            console.print(f"  Fetching CIDRs for [bold white]{c}[/] ({get_country_name(c)})...")
        fetch_country_zone(c, force=True)
        v4, v6 = get_country_cidrs(c, auto_fetch=False)
        if not silent and RICH_AVAILABLE:
            console.print(f"  [green]✔ {c}: {len(v4)} IPv4 CIDRs, {len(v6)} IPv6 CIDRs[/]")

    if not silent:
        if RICH_AVAILABLE:
            console.print("[bold green]● GeoIP zone cache update complete.[/]")
        else:
            print("[eFa-Firewall] GeoIP zone cache update complete.")

# ----------------- Native NFTables Generator & Manager -----------------
def format_nft_set(name, type_str, elements):
    lines = []
    lines.append(f"    set {name} {{")
    lines.append(f"        type {type_str}")
    lines.append("        flags interval")
    if elements:
        sorted_elems = sorted(list(elements))
        # Format elements cleanly
        elem_str = ", ".join(sorted_elems)
        lines.append(f"        elements = {{ {elem_str} }}")
    lines.append("    }")
    return lines

def setup_nftables_persistence():
    os.makedirs("/etc/nftables", exist_ok=True)
    inc_line = f'include "{NFT_EFA_FILE}"'
    
    # 1. Check RHEL / CentOS sysconfig
    rhel_conf = "/etc/sysconfig/nftables.conf"
    if os.path.exists(os.path.dirname(rhel_conf)):
        has_inc = False
        if os.path.exists(rhel_conf):
            with open(rhel_conf, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
                if inc_line in content or NFT_EFA_FILE in content:
                    has_inc = True
        if not has_inc:
            with open(rhel_conf, "a", encoding="utf-8") as f:
                f.write(f"\n# Include EFA-NG Firewall Rules\n{inc_line}\n")

    # 2. Check Debian / Ubuntu /etc/nftables.conf
    deb_conf = "/etc/nftables.conf"
    if os.path.exists(deb_conf):
        has_inc = False
        with open(deb_conf, "r", encoding="utf-8", errors="ignore") as f:
            content = f.read()
            if inc_line in content or NFT_EFA_FILE in content:
                has_inc = True
        if not has_inc:
            with open(deb_conf, "a", encoding="utf-8") as f:
                f.write(f"\n# Include EFA-NG Firewall Rules\n{inc_line}\n")

    # 3. Disable conflicting firewalld if present
    try:
        subprocess.run(["systemctl", "disable", "--now", "firewalld"], capture_output=True, text=True)
    except Exception:
        pass

    # 4. Enable and start nftables.service
    try:
        subprocess.run(["systemctl", "enable", "--now", "nftables"], capture_output=True, text=True)
    except Exception:
        pass

def apply_firewall(cfg, silent=False):
    if not silent:
        if RICH_AVAILABLE:
            console.print("[bold cyan]● Generating and applying native NFTables configuration...[/]")
        else:
            print("[eFa-Firewall] Generating and applying native NFTables configuration...")

    enabled = cfg.get("enabled", True)
    allow_ping = cfg.get("allow_ping", True)

    nft_lines = [
        "#!/usr/sbin/nft -f",
        "# +--------------------------------------------------------------------+",
        "# EFA-NG Native NFTables Configuration (Auto-Generated)",
        f"# Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}",
        "# +--------------------------------------------------------------------+",
        "",
        "table inet efa",
        "delete table inet efa",
        "",
        "table inet efa {"
    ]

    if not enabled:
        # Open / Unrestricted Mode
        nft_lines.extend([
            "    chain input {",
            "        type filter hook input priority filter; policy accept;",
            "    }",
            "    chain forward {",
            "        type filter hook forward priority filter; policy accept;",
            "    }",
            "    chain output {",
            "        type filter hook output priority filter; policy accept;",
            "    }",
            "}"
        ])
    else:
        # 1. Parse Static IPs
        admin_v4, admin_v6 = set(), set()
        for item in cfg.get("admins", []):
            norm, ver = normalize_ip(item.get("ip", ""))
            if ver == 4: admin_v4.add(norm)
            elif ver == 6: admin_v6.add(norm)
        admin_v4.add("127.0.0.1")
        admin_v6.add("::1")

        trusted_v4, trusted_v6 = set(), set()
        for item in cfg.get("trusted", {}).get("entries", []):
            norm, ver = normalize_ip(item.get("ip", ""))
            if ver == 4: trusted_v4.add(norm)
            elif ver == 6: trusted_v6.add(norm)

        http_v4, http_v6 = set(), set()
        for item in cfg.get("http_only", {}).get("entries", []):
            norm, ver = normalize_ip(item.get("ip", ""))
            if ver == 4: http_v4.add(norm)
            elif ver == 6: http_v6.add(norm)

        mail_v4, mail_v6 = set(), set()
        for item in cfg.get("mail_clients", {}).get("entries", []):
            norm, ver = normalize_ip(item.get("ip", ""))
            if ver == 4: mail_v4.add(norm)
            elif ver == 6: mail_v6.add(norm)

        # 2. Parse GeoIP
        geo_cfg = cfg.get("geoip", {})
        geo_enabled = geo_cfg.get("enabled", True)

        geo_bl_v4, geo_bl_v6 = set(), set()
        if geo_enabled:
            for item in geo_cfg.get("global_blacklist", []):
                cc = item.get("country", "")
                if cc:
                    v4, v6 = get_country_cidrs(cc)
                    geo_bl_v4.update(v4)
                    geo_bl_v6.update(v6)

        group_geo = geo_cfg.get("groups", {})
        def get_grp_geo(gname):
            gwl_v4, gwl_v6 = set(), set()
            gbl_v4, gbl_v6 = set(), set()
            if not geo_enabled: return "disabled", gwl_v4, gwl_v6, gbl_v4, gbl_v6
            gdata = group_geo.get(gname, {})
            mode = gdata.get("mode", "disabled").lower()
            for it in gdata.get("countries", []):
                cc = it.get("country", "")
                if cc:
                    v4, v6 = get_country_cidrs(cc)
                    if mode == "whitelist":
                        gwl_v4.update(v4); gwl_v6.update(v6)
                    elif mode == "blacklist":
                        gbl_v4.update(v4); gbl_v6.update(v6)
            return mode, gwl_v4, gwl_v6, gbl_v4, gbl_v6

        http_mode, http_gwl_v4, http_gwl_v6, http_gbl_v4, http_gbl_v6 = get_grp_geo("http_only")
        mail_mode, mail_gwl_v4, mail_gwl_v6, mail_gbl_v4, mail_gbl_v6 = get_grp_geo("mail_clients")
        trust_mode, trust_gwl_v4, trust_gwl_v6, trust_gbl_v4, trust_gbl_v6 = get_grp_geo("trusted")
        pub_mode, pub_gwl_v4, pub_gwl_v6, pub_gbl_v4, pub_gbl_v6 = get_grp_geo("public_ports")

        # 3. Add NFTables Sets
        nft_lines.extend(format_nft_set("admins_v4", "ipv4_addr", admin_v4))
        nft_lines.extend(format_nft_set("admins_v6", "ipv6_addr", admin_v6))
        nft_lines.extend(format_nft_set("trusted_v4", "ipv4_addr", trusted_v4))
        nft_lines.extend(format_nft_set("trusted_v6", "ipv6_addr", trusted_v6))
        nft_lines.extend(format_nft_set("http_v4", "ipv4_addr", http_v4))
        nft_lines.extend(format_nft_set("http_v6", "ipv6_addr", http_v6))
        nft_lines.extend(format_nft_set("mail_v4", "ipv4_addr", mail_v4))
        nft_lines.extend(format_nft_set("mail_v6", "ipv6_addr", mail_v6))

        if geo_bl_v4 or geo_enabled:
            nft_lines.extend(format_nft_set("geo_block_v4", "ipv4_addr", geo_bl_v4))
        if geo_bl_v6 or geo_enabled:
            nft_lines.extend(format_nft_set("geo_block_v6", "ipv6_addr", geo_bl_v6))

        if http_mode == "whitelist":
            nft_lines.extend(format_nft_set("geo_http_wl_v4", "ipv4_addr", http_gwl_v4))
            nft_lines.extend(format_nft_set("geo_http_wl_v6", "ipv6_addr", http_gwl_v6))
        elif http_mode == "blacklist":
            nft_lines.extend(format_nft_set("geo_http_bl_v4", "ipv4_addr", http_gbl_v4))
            nft_lines.extend(format_nft_set("geo_http_bl_v6", "ipv6_addr", http_gbl_v6))

        if mail_mode == "whitelist":
            nft_lines.extend(format_nft_set("geo_mail_wl_v4", "ipv4_addr", mail_gwl_v4))
            nft_lines.extend(format_nft_set("geo_mail_wl_v6", "ipv6_addr", mail_gwl_v6))
        elif mail_mode == "blacklist":
            nft_lines.extend(format_nft_set("geo_mail_bl_v4", "ipv4_addr", mail_gbl_v4))
            nft_lines.extend(format_nft_set("geo_mail_bl_v6", "ipv6_addr", mail_gbl_v6))

        if trust_mode == "whitelist":
            nft_lines.extend(format_nft_set("geo_trust_wl_v4", "ipv4_addr", trust_gwl_v4))
            nft_lines.extend(format_nft_set("geo_trust_wl_v6", "ipv6_addr", trust_gwl_v6))
        elif trust_mode == "blacklist":
            nft_lines.extend(format_nft_set("geo_trust_bl_v4", "ipv4_addr", trust_gbl_v4))
            nft_lines.extend(format_nft_set("geo_trust_bl_v6", "ipv6_addr", trust_gbl_v6))

        if pub_mode == "whitelist":
            nft_lines.extend(format_nft_set("geo_pub_wl_v4", "ipv4_addr", pub_gwl_v4))
            nft_lines.extend(format_nft_set("geo_pub_wl_v6", "ipv6_addr", pub_gwl_v6))
        elif pub_mode == "blacklist":
            nft_lines.extend(format_nft_set("geo_pub_bl_v4", "ipv4_addr", pub_gbl_v4))
            nft_lines.extend(format_nft_set("geo_pub_bl_v6", "ipv6_addr", pub_gbl_v6))

        # Helper to format port strings
        def extract_ports(port_list, proto="tcp"):
            p_nums = []
            for p in port_list:
                if p.get("proto", "tcp").lower() == proto.lower():
                    p_num = str(p.get("port", "")).strip()
                    if p_num: p_nums.append(p_num)
            return p_nums

        # 4. Chain Input Rules
        nft_lines.append("")
        nft_lines.append("    chain input {")
        nft_lines.append("        type filter hook input priority filter; policy drop;")
        nft_lines.append("")
        nft_lines.append("        # 1. Established & Related state")
        nft_lines.append("        ct state established,related accept")
        nft_lines.append("        ct state invalid drop")
        nft_lines.append("")
        nft_lines.append("        # 2. Local loopback")
        nft_lines.append('        iif "lo" accept')
        nft_lines.append("")

        if allow_ping:
            nft_lines.append("        # 3. ICMP / Ping")
            nft_lines.append("        ip protocol icmp accept")
            nft_lines.append("        ip6 nexthdr ipv6-icmp accept")
            nft_lines.append("")

        nft_lines.append("        # 4. DHCPv6 Client")
        nft_lines.append("        ip6 daddr fe80::/64 udp dport 546 accept")
        nft_lines.append("")

        nft_lines.append("        # 5. Admins Full Access (Takes precedence)")
        nft_lines.append("        ip saddr @admins_v4 accept")
        nft_lines.append("        ip6 saddr @admins_v6 accept")
        nft_lines.append("")

        if geo_bl_v4 or geo_bl_v6:
            nft_lines.append("        # 6. Global Country Blacklist (Drop on ALL Ports)")
            if geo_bl_v4: nft_lines.append("        ip saddr @geo_block_v4 drop")
            if geo_bl_v6: nft_lines.append("        ip6 saddr @geo_block_v6 drop")
            nft_lines.append("")

        # 7. Group Blacklists
        http_tcp_ports = extract_ports(cfg.get("http_only", {}).get("ports", [{"port": "80"}, {"port": "443"}]), "tcp")
        mail_tcp_ports = extract_ports(cfg.get("mail_clients", {}).get("ports", [{"port": "110"}, {"port": "143"}, {"port": "587"}, {"port": "993"}, {"port": "995"}]), "tcp")
        trust_tcp_ports = extract_ports(cfg.get("trusted", {}).get("ports", [{"port": "3306"}, {"port": "10050"}]), "tcp")
        pub_tcp_ports = extract_ports(cfg.get("public_ports", [{"port": "25"}]), "tcp")

        if http_mode == "blacklist" and (http_gbl_v4 or http_gbl_v6) and http_tcp_ports:
            nft_lines.append(f"        # Group Blacklist: HTTP")
            p_str = ", ".join(http_tcp_ports)
            if http_gbl_v4: nft_lines.append(f"        ip saddr @geo_http_bl_v4 tcp dport {{ {p_str} }} drop")
            if http_gbl_v6: nft_lines.append(f"        ip6 saddr @geo_http_bl_v6 tcp dport {{ {p_str} }} drop")

        if mail_mode == "blacklist" and (mail_gbl_v4 or mail_gbl_v6) and mail_tcp_ports:
            nft_lines.append(f"        # Group Blacklist: Mail Clients")
            p_str = ", ".join(mail_tcp_ports)
            if mail_gbl_v4: nft_lines.append(f"        ip saddr @geo_mail_bl_v4 tcp dport {{ {p_str} }} drop")
            if mail_gbl_v6: nft_lines.append(f"        ip6 saddr @geo_mail_bl_v6 tcp dport {{ {p_str} }} drop")

        if trust_mode == "blacklist" and (trust_gbl_v4 or trust_gbl_v6) and trust_tcp_ports:
            nft_lines.append(f"        # Group Blacklist: Trusted")
            p_str = ", ".join(trust_tcp_ports)
            if trust_gbl_v4: nft_lines.append(f"        ip saddr @geo_trust_bl_v4 tcp dport {{ {p_str} }} drop")
            if trust_gbl_v6: nft_lines.append(f"        ip6 saddr @geo_trust_bl_v6 tcp dport {{ {p_str} }} drop")

        if pub_mode == "blacklist" and (pub_gbl_v4 or pub_gbl_v6) and pub_tcp_ports:
            nft_lines.append(f"        # Group Blacklist: Public Ports")
            p_str = ", ".join(pub_tcp_ports)
            if pub_gbl_v4: nft_lines.append(f"        ip saddr @geo_pub_bl_v4 tcp dport {{ {p_str} }} drop")
            if pub_gbl_v6: nft_lines.append(f"        ip6 saddr @geo_pub_bl_v6 tcp dport {{ {p_str} }} drop")

        # 8. Public Ports
        if pub_tcp_ports:
            p_str = ", ".join(pub_tcp_ports)
            if pub_mode == "whitelist":
                nft_lines.append("        # Public Ports (Restricted to Whitelist Countries)")
                if pub_gwl_v4: nft_lines.append(f"        ip saddr @geo_pub_wl_v4 tcp dport {{ {p_str} }} accept")
                if pub_gwl_v6: nft_lines.append(f"        ip6 saddr @geo_pub_wl_v6 tcp dport {{ {p_str} }} accept")
            else:
                nft_lines.append(f"        # Public World Ports (Open to 0.0.0.0/0)")
                nft_lines.append(f"        tcp dport {{ {p_str} }} accept")

        # 9. HTTP / HTTPS Web Management
        if http_tcp_ports:
            p_str = ", ".join(http_tcp_ports)
            nft_lines.append("        # HTTP / HTTPS Web Management")
            nft_lines.append(f"        ip saddr @http_v4 tcp dport {{ {p_str} }} accept")
            nft_lines.append(f"        ip6 saddr @http_v6 tcp dport {{ {p_str} }} accept")
            if http_mode == "whitelist":
                if http_gwl_v4: nft_lines.append(f"        ip saddr @geo_http_wl_v4 tcp dport {{ {p_str} }} accept")
                if http_gwl_v6: nft_lines.append(f"        ip6 saddr @geo_http_wl_v6 tcp dport {{ {p_str} }} accept")

        # 10. Mail Clients Access
        if mail_tcp_ports:
            p_str = ", ".join(mail_tcp_ports)
            nft_lines.append("        # Mail Clients & Submission")
            nft_lines.append(f"        ip saddr @mail_v4 tcp dport {{ {p_str} }} accept")
            nft_lines.append(f"        ip6 saddr @mail_v6 tcp dport {{ {p_str} }} accept")
            if mail_mode == "whitelist":
                if mail_gwl_v4: nft_lines.append(f"        ip saddr @geo_mail_wl_v4 tcp dport {{ {p_str} }} accept")
                if mail_gwl_v6: nft_lines.append(f"        ip6 saddr @geo_mail_wl_v6 tcp dport {{ {p_str} }} accept")

        # 11. Trusted Services
        if trust_tcp_ports:
            p_str = ", ".join(trust_tcp_ports)
            nft_lines.append("        # Trusted Services (MariaDB, Zabbix)")
            nft_lines.append(f"        ip saddr @trusted_v4 tcp dport {{ {p_str} }} accept")
            nft_lines.append(f"        ip6 saddr @trusted_v6 tcp dport {{ {p_str} }} accept")
            if trust_mode == "whitelist":
                if trust_gwl_v4: nft_lines.append(f"        ip saddr @geo_trust_wl_v4 tcp dport {{ {p_str} }} accept")
                if trust_gwl_v6: nft_lines.append(f"        ip6 saddr @geo_trust_wl_v6 tcp dport {{ {p_str} }} accept")

        nft_lines.append("    }")
        nft_lines.append("")
        nft_lines.append("    chain forward {")
        nft_lines.append("        type filter hook forward priority filter; policy accept;")
        nft_lines.append("    }")
        nft_lines.append("")
        nft_lines.append("    chain output {")
        nft_lines.append("        type filter hook output priority filter; policy accept;")
        nft_lines.append("    }")
        nft_lines.append("}")

    # Write file
    os.makedirs(os.path.dirname(NFT_EFA_FILE), exist_ok=True)
    tmp_file = NFT_EFA_FILE + ".tmp"
    with open(tmp_file, "w", encoding="utf-8") as f:
        f.write("\n".join(nft_lines) + "\n")
    os.chmod(tmp_file, 0o644)

    # Syntax check
    check_res = subprocess.run(["nft", "-c", "-f", tmp_file], capture_output=True, text=True)
    if check_res.returncode != 0:
        if RICH_AVAILABLE:
            console.print(f"[bold red]ERROR:[/] NFTables syntax verification failed:\n{check_res.stderr}")
        else:
            print(f"[ERROR] NFTables syntax verification failed:\n{check_res.stderr}")
        if os.path.exists(tmp_file): os.remove(tmp_file)
        return False

    os.replace(tmp_file, NFT_EFA_FILE)

    # Apply configuration atomically
    apply_res = subprocess.run(["nft", "-f", NFT_EFA_FILE], capture_output=True, text=True)
    if apply_res.returncode != 0:
        if RICH_AVAILABLE:
            console.print(f"[bold red]ERROR:[/] Failed to apply NFTables rules:\n{apply_res.stderr}")
        else:
            print(f"[ERROR] Failed to apply NFTables rules:\n{apply_res.stderr}")
        return False

    # Setup boot persistence and systemd service
    setup_nftables_persistence()

    if not silent:
        if RICH_AVAILABLE:
            console.print("[bold green]● NFTables firewall rules generated and applied successfully.[/]")
        else:
            print("[eFa-Firewall] NFTables firewall rules generated and applied successfully.")
    return True

# ----------------- Status and List Displays -----------------
def is_nftables_active():
    try:
        res = subprocess.run(["nft", "list", "table", "inet", "efa"], capture_output=True, text=True)
        return res.returncode == 0
    except Exception:
        return False

def show_status(cfg):
    is_active = is_nftables_active()
    enabled = cfg.get("enabled", True)
    
    if not RICH_AVAILABLE:
        print("Firewall Engine: NFTables (Native)")
        print("Status:", "ACTIVE" if is_active else "INACTIVE")
        print("Mode:", "PROTECTED" if enabled else "OPEN")
        return

    # Header Panel
    header_text = Text()
    header_text.append("EFA-NG FIREWALL & ACCESS CONTROL MANAGER", style="bold cyan")
    header_text.append("\nNative Linux NFTables Engine  •  CentOS / RHEL / Debian / Ubuntu  •  https://efa-ng.space.ua", style="dim white")
    console.print(Panel(Align.center(header_text), box=box.ROUNDED, border_style="cyan"))

    # Status Cards
    fw_badge = "[bold green]● ACTIVE (Running in Kernel)[/]" if is_active else "[bold red]○ INACTIVE (Not Applied)[/]"
    mode_badge = "[bold green]● PROTECTED (Restricted ACL)[/]" if enabled else "[bold yellow]○ UNRESTRICTED (Open Mode)[/]"
    
    num_admins = len(cfg.get("admins", []))
    num_trusted = len(cfg.get("trusted", {}).get("entries", []))
    num_http = len(cfg.get("http_only", {}).get("entries", []))
    num_mail = len(cfg.get("mail_clients", {}).get("entries", []))
    num_pub = len(cfg.get("public_ports", []))

    # GeoIP summary
    geo_cfg = cfg.get("geoip", {})
    geo_gbl = [item.get("country", "") for item in geo_cfg.get("global_blacklist", []) if item.get("country")]
    geo_gbl_str = f"[bold red]● {len(geo_gbl)} Countries Blocked[/] ({', '.join(geo_gbl[:6])}{'...' if len(geo_gbl)>6 else ''})" if geo_gbl else "[dim]None configured[/]"

    http_geo = geo_cfg.get("groups", {}).get("http_only", {})
    http_geo_mode = http_geo.get("mode", "disabled").upper()
    http_geo_cnt = [c.get("country", "") for c in http_geo.get("countries", []) if c.get("country")]
    http_geo_str = f"[{'bold green' if http_geo_mode=='WHITELIST' else 'bold yellow'}]{http_geo_mode}[/] ({', '.join(http_geo_cnt)})" if http_geo_mode != "DISABLED" else "[dim]Disabled (Static ACL only)[/]"

    mail_geo = geo_cfg.get("groups", {}).get("mail_clients", {})
    mail_geo_mode = mail_geo.get("mode", "disabled").upper()
    mail_geo_cnt = [c.get("country", "") for c in mail_geo.get("countries", []) if c.get("country")]
    mail_geo_str = f"[{'bold green' if mail_geo_mode=='WHITELIST' else 'bold yellow'}]{mail_geo_mode}[/] ({', '.join(mail_geo_cnt)})" if mail_geo_mode != "DISABLED" else "[dim]Disabled (Static ACL only)[/]"

    summary_table = Table(box=box.SIMPLE_HEAVY, show_header=False, expand=True)
    summary_table.add_column("Key", style="bold cyan", width=32, min_width=32, max_width=32, no_wrap=True)
    summary_table.add_column("Value", style="bold white", ratio=1)

    summary_table.add_row("●  Firewall Engine", "[bold green]NFTables (Direct Netfilter)[/]")
    summary_table.add_row("●  Kernel Table State", fw_badge)
    summary_table.add_row("●  Protection Mode", mode_badge)
    summary_table.add_row("●  Admins (All Ports Open)", f"[bold white]{num_admins}[/] networks / hosts")
    summary_table.add_row("●  Trusted (DB & Zabbix)", f"[bold white]{num_trusted}[/] networks (Ports: 3306, 10050)")
    summary_table.add_row("●  HTTP / HTTPS Web Access", f"[bold white]{num_http}[/] networks (Ports: 80, 443)")
    summary_table.add_row("●  Mail Clients & Submission", f"[bold white]{num_mail}[/] networks (Ports: 110, 143, 587, 993, 995)")
    summary_table.add_row("●  Public World Access", f"[bold white]{num_pub}[/] ports (Port: 25/tcp SMTP)")
    summary_table.add_row("●  Global Country Blacklist", geo_gbl_str)
    summary_table.add_row("●  HTTP / HTTPS GeoIP Filter", http_geo_str)
    summary_table.add_row("●  Mail Clients GeoIP Filter", mail_geo_str)

    console.print(Panel(summary_table, title="[bold white]● System Status Overview[/]", box=box.ROUNDED, border_style="blue"))

def create_rules_table(title, header_style, border_style):
    t = Table(title=title, box=box.ROUNDED, header_style=header_style, border_style=border_style, expand=True)
    t.add_column("[ # ]", justify="center", style="bold cyan", width=7, min_width=7, max_width=7, no_wrap=True)
    t.add_column("IP / CIDR Network", style="bold white", width=22, min_width=22, max_width=22, no_wrap=True)
    t.add_column("Allowed Ports", style="bold green", width=26, min_width=26, max_width=26, no_wrap=True)
    t.add_column("Description / Label", style="italic yellow", ratio=1)
    return t

def list_rules(cfg):
    show_status(cfg)
    
    if not RICH_AVAILABLE:
        return

    # 1. Admins Table
    t_admin = create_rules_table("● ADMINS ACCESS LIST (Full Access to All Ports & Protocols)", "bold magenta", "magenta")
    admins = cfg.get("admins", [])
    if not admins:
        t_admin.add_row("[ - ]", "None configured", "-", "-")
    else:
        for idx, item in enumerate(admins, 1):
            t_admin.add_row(f"[ {idx:2d} ]", item.get("ip", ""), "ALL (1-65535 TCP/UDP)", item.get("comment", ""))
    console.print(t_admin)

    # 2. Trusted Table
    t_trust = create_rules_table("● TRUSTED SERVICES (MariaDB 3306, Zabbix 10050)", "bold blue", "blue")
    trusted_entries = cfg.get("trusted", {}).get("entries", [])
    if not trusted_entries:
        t_trust.add_row("[ - ]", "None configured", "-", "-")
    else:
        for idx, item in enumerate(trusted_entries, 1):
            t_trust.add_row(f"[ {idx:2d} ]", item.get("ip", ""), "3306/tcp, 10050/tcp", item.get("comment", ""))
    console.print(t_trust)

    # 3. HTTP Only Table
    t_http = create_rules_table("● HTTP / HTTPS WEB ACCESS (Ports 80, 443)", "bold green", "green")
    http_entries = cfg.get("http_only", {}).get("entries", [])
    if not http_entries:
        t_http.add_row("[ - ]", "None configured", "-", "-")
    else:
        for idx, item in enumerate(http_entries, 1):
            t_http.add_row(f"[ {idx:2d} ]", item.get("ip", ""), "80/tcp, 443/tcp", item.get("comment", ""))
    console.print(t_http)

    # 4. Mail Clients Table
    t_mail = create_rules_table("● MAIL CLIENTS & SUBMISSION (Ports 110, 143, 587, 993, 995)", "bold dark_cyan", "dark_cyan")
    mail_entries = cfg.get("mail_clients", {}).get("entries", [])
    if not mail_entries:
        t_mail.add_row("[ - ]", "None configured", "-", "-")
    else:
        for idx, item in enumerate(mail_entries, 1):
            t_mail.add_row(f"[ {idx:2d} ]", item.get("ip", ""), "110, 143, 587, 993, 995", item.get("comment", ""))
    console.print(t_mail)

    # 5. Public Ports Table
    t_pub = create_rules_table("● PUBLIC PORTS (Open to 0.0.0.0/0 World)", "bold yellow", "yellow")
    pub_ports = cfg.get("public_ports", [])
    if not pub_ports:
        t_pub.add_row("[ - ]", "None configured", "-", "-")
    else:
        for idx, item in enumerate(pub_ports, 1):
            p = f"{item.get('port')}/{item.get('proto', 'tcp')}"
            t_pub.add_row(f"[ {idx:2d} ]", "0.0.0.0/0 (Any)", p, item.get("comment", ""))
    console.print(t_pub)

    # 6. GeoIP Global Blacklist Table
    t_geo_bl = Table(title="● GEOIP GLOBAL BLACKLIST (Dropped on ALL Ports for Entire Server)", box=box.ROUNDED, header_style="bold red", border_style="red", expand=True)
    t_geo_bl.add_column("[ # ]", justify="center", style="bold cyan", width=7, min_width=7, max_width=7, no_wrap=True)
    t_geo_bl.add_column("Country / Region", style="bold white", width=22, min_width=22, max_width=22, no_wrap=True)
    t_geo_bl.add_column("Policy / Action", style="bold red", width=26, min_width=26, max_width=26, no_wrap=True)
    t_geo_bl.add_column("Description / Label", style="italic yellow", ratio=1)

    gbl_entries = cfg.get("geoip", {}).get("global_blacklist", [])
    if not gbl_entries:
        t_geo_bl.add_row("[ - ]", "None configured", "-", "-")
    else:
        for idx, item in enumerate(gbl_entries, 1):
            cc = item.get("country", "").upper()
            cname = get_country_name(cc)
            t_geo_bl.add_row(f"[ {idx:2d} ]", f"{cc} ({cname})", "DROP ALL TRAFFIC", item.get("comment", ""))
    console.print(t_geo_bl)

    # 7. GeoIP Group Policies Table
    t_geo_grp = Table(title="● GEOIP PER-GROUP ACCESS POLICIES (Whitelist / Blacklist)", box=box.ROUNDED, header_style="bold bright_cyan", border_style="bright_cyan", expand=True)
    t_geo_grp.add_column("[ # ]", justify="center", style="bold cyan", width=7, min_width=7, max_width=7, no_wrap=True)
    t_geo_grp.add_column("Group / Service", style="bold white", width=22, min_width=22, max_width=22, no_wrap=True)
    t_geo_grp.add_column("Filter Mode", style="bold green", width=26, min_width=26, max_width=26, no_wrap=True)
    t_geo_grp.add_column("Countries / Rules", style="italic yellow", ratio=1)

    groups_info = [
        ("http_only", "HTTP / HTTPS (80, 443)"),
        ("mail_clients", "Mail Clients (110..995)"),
        ("public_ports", "Public Ports (25 SMTP)"),
        ("trusted", "Trusted (3306, 10050)")
    ]
    for idx, (gkey, glabel) in enumerate(groups_info, 1):
        gdata = cfg.get("geoip", {}).get("groups", {}).get(gkey, {})
        gmode = gdata.get("mode", "disabled").upper()
        clist = [f"{c.get('country')}" for c in gdata.get("countries", []) if c.get("country")]
        c_str = f"Allowed: {', '.join(clist)}" if gmode == "WHITELIST" else (f"Blocked: {', '.join(clist)}" if gmode == "BLACKLIST" else "Disabled (Static ACL only)")
        mode_styled = f"[bold green]{gmode}[/]" if gmode == "WHITELIST" else (f"[bold red]{gmode}[/]" if gmode == "BLACKLIST" else "[dim]DISABLED[/]")
        t_geo_grp.add_row(f"[ {idx:2d} ]", glabel, mode_styled, c_str)
    console.print(t_geo_grp)
    console.print("")

# ----------------- CLI Management Functions -----------------
def cli_add_entry(cfg, group, ip, comment=""):
    norm, ver = normalize_ip(ip)
    if not norm:
        if RICH_AVAILABLE:
            console.print(f"[bold red]ERROR:[/] Invalid IP address or CIDR network: '{ip}'")
        else:
            print(f"[ERROR] Invalid IP: '{ip}'")
        return False

    group = group.lower()
    if group in ["admin", "admins"]:
        entries = cfg.setdefault("admins", [])
    elif group in ["trusted"]:
        entries = cfg.setdefault("trusted", {}).setdefault("entries", [])
    elif group in ["http", "http_only", "http-only", "web"]:
        entries = cfg.setdefault("http_only", {}).setdefault("entries", [])
    elif group in ["mail", "mail_clients", "mail-clients", "mail_access", "mail_users", "pop", "imap", "submission"]:
        entries = cfg.setdefault("mail_clients", {}).setdefault("entries", [])
    else:
        if RICH_AVAILABLE:
            console.print(f"[bold red]ERROR:[/] Unknown group '{group}'. Valid: admins, trusted, http_only, mail_clients")
        else:
            print(f"[ERROR] Unknown group '{group}'")
        return False

    for item in entries:
        if item.get("ip") == norm:
            item["comment"] = comment or item.get("comment", "")
            if RICH_AVAILABLE:
                console.print(f"[bold yellow]● Updated existing entry {norm} in {group}: {item['comment']}[/]")
            else:
                print(f"Updated {norm} in {group}")
            save_config(cfg)
            apply_firewall(cfg)
            return True

    entries.append({"ip": norm, "comment": comment})
    save_config(cfg)
    if RICH_AVAILABLE:
        console.print(f"[bold green]● Added {norm} to {group} with comment: '{comment}'[/]")
    else:
        print(f"Added {norm} to {group}")
    apply_firewall(cfg)
    return True

def cli_del_entry(cfg, group, ip):
    norm, ver = normalize_ip(ip)
    target = norm if norm else ip.strip()

    group = group.lower()
    if group in ["admin", "admins"]:
        entries = cfg.get("admins", [])
    elif group in ["trusted"]:
        entries = cfg.get("trusted", {}).get("entries", [])
    elif group in ["http", "http_only", "http-only", "web"]:
        entries = cfg.get("http_only", {}).get("entries", [])
    elif group in ["mail", "mail_clients", "mail-clients", "mail_access", "mail_users", "pop", "imap", "submission"]:
        entries = cfg.get("mail_clients", {}).get("entries", [])
    else:
        if RICH_AVAILABLE:
            console.print(f"[bold red]ERROR:[/] Unknown group '{group}'. Valid: admins, trusted, http_only, mail_clients")
        else:
            print(f"[ERROR] Unknown group '{group}'")
        return False

    found = False
    for i, item in enumerate(entries):
        if item.get("ip") == target:
            del entries[i]
            found = True
            break

    if found:
        save_config(cfg)
        if RICH_AVAILABLE:
            console.print(f"[bold green]● Deleted {target} from {group}.[/]")
        else:
            print(f"Deleted {target} from {group}")
        apply_firewall(cfg)
        return True
    else:
        if RICH_AVAILABLE:
            console.print(f"[bold red]ERROR:[/] Entry {target} not found in {group}.")
        else:
            print(f"[ERROR] Entry {target} not found in {group}")
        return False

# ----------------- Interactive Rich TUI Submenus -----------------
def manage_group_tui(cfg, group_name, title, desc, header_style="bold cyan", border_style="cyan"):
    while True:
        os.system("clear")
        
        header_text = Text()
        header_text.append(f"{title}\n", style=header_style)
        header_text.append(desc, style="dim white")
        console.print(Panel(Align.center(header_text), box=box.ROUNDED, border_style=border_style))

        if group_name == "admins":
            entries = cfg.setdefault("admins", [])
        elif group_name == "trusted":
            entries = cfg.setdefault("trusted", {}).setdefault("entries", [])
        elif group_name == "http_only":
            entries = cfg.setdefault("http_only", {}).setdefault("entries", [])
        elif group_name == "mail_clients":
            entries = cfg.setdefault("mail_clients", {}).setdefault("entries", [])
        elif group_name == "public_ports":
            entries = cfg.setdefault("public_ports", [])

        # Entries Table
        t = Table(box=box.ROUNDED, header_style=header_style, border_style=border_style, expand=True)
        t.add_column("[ # ]", justify="center", style="bold cyan", width=7, min_width=7, max_width=7, no_wrap=True)
        if group_name == "public_ports":
            t.add_column("IP / CIDR Network", style="bold white", width=22, min_width=22, max_width=22, no_wrap=True)
            t.add_column("Allowed Ports", style="bold green", width=26, min_width=26, max_width=26, no_wrap=True)
        else:
            t.add_column("IP / CIDR Network", style="bold white", width=22, min_width=22, max_width=22, no_wrap=True)
            t.add_column("Family", style="bold green", width=12, min_width=12, max_width=12, no_wrap=True)
        t.add_column("Comment / Label", style="italic yellow", ratio=1)

        if not entries:
            t.add_row("[ - ]", "No entries configured", "-", "-")
        else:
            for idx, item in enumerate(entries, 1):
                if group_name == "public_ports":
                    p = f"{item.get('port')}/{item.get('proto', 'tcp')}"
                    t.add_row(f"[ {idx:2d} ]", "0.0.0.0/0 (Any)", p, item.get("comment", ""))
                else:
                    ip_val = item.get("ip", "")
                    _, ver = normalize_ip(ip_val)
                    ver_str = f"IPv{ver}" if ver else "IP"
                    t.add_row(f"[ {idx:2d} ]", ip_val, ver_str, item.get("comment", ""))

        console.print(t)
        
        # Actions Panel
        action_table = Table(box=box.SIMPLE, show_header=False, expand=True)
        action_table.add_column("Key", style="bold cyan", width=10, justify="center")
        action_table.add_column("Action", style="white")
        
        action_table.add_row("[ A ]", "● Add new IP / CIDR entry with description")
        action_table.add_row("[ E ]", "● Edit comment / description of an entry")
        action_table.add_row("[ D ]", "● Delete an entry")
        action_table.add_row("[ B ]", "● Back to Main Menu")
        
        console.print(Panel(action_table, title="[bold white]● Available Actions[/]", box=box.ROUNDED, border_style="dim white"))

        choice = Prompt.ask("[bold green]Choose action [A/E/D/B][/]", choices=["a", "e", "d", "b", "q", ""], default="b").strip().lower()

        if choice in ["b", "q", ""]:
            break
        elif choice == "a":
            if group_name == "public_ports":
                port = Prompt.ask("Enter port number (e.g. 25, 587)").strip()
                if not port.isdigit():
                    console.print("[bold red]Invalid port number.[/]")
                    Prompt.ask("Press Enter to continue...")
                    continue
                proto = Prompt.ask("Enter protocol", choices=["tcp", "udp"], default="tcp").strip().lower()
                comm = Prompt.ask("Enter comment / description").strip()
                entries.append({"port": port, "proto": proto, "comment": comm})
                save_config(cfg)
                apply_firewall(cfg)
                Prompt.ask("[bold green]● Entry added and applied! Press Enter...[/]")
            else:
                ip_in = Prompt.ask("Enter IP address or CIDR subnet (e.g. 192.168.1.50 or 10.0.0.0/24)").strip()
                norm, ver = normalize_ip(ip_in)
                if not norm:
                    console.print("[bold red]Invalid IP or CIDR network format.[/]")
                    Prompt.ask("Press Enter to continue...")
                    continue
                comm = Prompt.ask("Enter comment / description (e.g. Office LAN / Client Subnet)").strip()
                entries.append({"ip": norm, "comment": comm})
                save_config(cfg)
                apply_firewall(cfg)
                Prompt.ask("[bold green]● Entry added and applied! Press Enter...[/]")
        elif choice == "e":
            if not entries:
                continue
            idx_in = Prompt.ask(f"Enter entry number to edit comment [1-{len(entries)}]").strip()
            if idx_in.isdigit() and 1 <= int(idx_in) <= len(entries):
                target_item = entries[int(idx_in)-1]
                old_comm = target_item.get("comment", "")
                console.print(f"Current description: [yellow]{old_comm}[/]")
                new_comm = Prompt.ask("Enter new description", default=old_comm).strip()
                if new_comm:
                    target_item["comment"] = new_comm
                    save_config(cfg)
                    Prompt.ask("[bold green]● Comment updated! Press Enter...[/]")
            else:
                console.print("[bold red]Invalid entry number.[/]")
                Prompt.ask("Press Enter to continue...")
        elif choice == "d":
            if not entries:
                continue
            idx_in = Prompt.ask(f"Enter entry number to delete [1-{len(entries)}]").strip()
            if idx_in.isdigit() and 1 <= int(idx_in) <= len(entries):
                target_item = entries[int(idx_in)-1]
                del_desc = target_item.get("ip") or target_item.get("port")
                if Confirm.ask(f"Are you sure you want to delete [bold red]{del_desc}[/]?", default=False):
                    entries.pop(int(idx_in)-1)
                    save_config(cfg)
                    apply_firewall(cfg)
                    Prompt.ask("[bold green]● Deleted entry and reloaded firewall. Press Enter...[/]")
            else:
                console.print("[bold red]Invalid entry number.[/]")
                Prompt.ask("Press Enter to continue...")

def manage_global_blacklist_tui(cfg):
    while True:
        os.system("clear")
        header_text = Text()
        header_text.append("● GEOIP GLOBAL COUNTRY BLACKLIST\n", style="bold red")
        header_text.append("Traffic from these countries will be unconditionally DROPPED across all ports for entire server", style="dim white")
        console.print(Panel(Align.center(header_text), box=box.ROUNDED, border_style="red"))

        entries = cfg.setdefault("geoip", {}).setdefault("global_blacklist", [])

        t = Table(box=box.ROUNDED, header_style="bold red", border_style="red", expand=True)
        t.add_column("[ # ]", justify="center", style="bold cyan", width=7, min_width=7, max_width=7, no_wrap=True)
        t.add_column("Country / Code", style="bold white", width=22, min_width=22, max_width=22, no_wrap=True)
        t.add_column("Action", style="bold red", width=26, min_width=26, max_width=26, no_wrap=True)
        t.add_column("Description / Label", style="italic yellow", ratio=1)

        if not entries:
            t.add_row("[ - ]", "No countries blocked", "-", "-")
        else:
            for idx, item in enumerate(entries, 1):
                cc = item.get("country", "").upper()
                cname = get_country_name(cc)
                t.add_row(f"[ {idx:2d} ]", f"{cc} ({cname})", "DROP ALL TRAFFIC", item.get("comment", ""))

        console.print(t)

        action_table = Table(box=box.SIMPLE, show_header=False, expand=True)
        action_table.add_column("Key", style="bold cyan", width=10, justify="center")
        action_table.add_column("Action", style="white")
        action_table.add_row("[ A ]", "● Add country to Global Blacklist (e.g. RU, CN, BR)")
        action_table.add_row("[ E ]", "● Edit description / comment of an entry")
        action_table.add_row("[ D ]", "● Remove country from Global Blacklist")
        action_table.add_row("[ B ]", "● Back to GeoIP Menu")
        console.print(Panel(action_table, title="[bold white]● Available Actions[/]", box=box.ROUNDED, border_style="dim white"))

        choice = Prompt.ask("[bold green]Choose action [A/E/D/B][/]", choices=["a", "e", "d", "b", "q", ""], default="b").strip().lower()

        if choice in ["b", "q", ""]:
            break
        elif choice == "a":
            cc_in = Prompt.ask("Enter 2-letter Country Code (e.g. RU, CN, BR, IR, KP)").strip().upper()
            if len(cc_in) != 2 or not cc_in.isalpha():
                console.print("[bold red]Invalid country code. Must be 2 letters (e.g. RU).[/]")
                Prompt.ask("Press Enter...")
                continue
            cname = get_country_name(cc_in)
            comm = Prompt.ask(f"Enter description / reason (Country: {cname})", default=f"{cname} (Blocked globally)").strip()
            
            console.print(f"[cyan]Fetching CIDRs for {cc_in} ({cname})...[/]")
            fetch_country_zone(cc_in)
            v4, v6 = get_country_cidrs(cc_in)
            console.print(f"[green]✔ Cached {len(v4)} IPv4 CIDRs and {len(v6)} IPv6 CIDRs for {cc_in}.[/]")

            found = False
            for it in entries:
                if it.get("country", "").upper() == cc_in:
                    it["comment"] = comm
                    found = True
                    break
            if not found:
                entries.append({"country": cc_in, "comment": comm})
            
            save_config(cfg)
            apply_firewall(cfg)
            Prompt.ask("[bold green]● Country added to Global Blacklist! Press Enter...[/]")
        elif choice == "e":
            if not entries:
                continue
            idx_in = Prompt.ask(f"Enter number to edit [1-{len(entries)}]").strip()
            if idx_in.isdigit() and 1 <= int(idx_in) <= len(entries):
                item = entries[int(idx_in)-1]
                new_comm = Prompt.ask("Enter new description", default=item.get("comment", "")).strip()
                if new_comm:
                    item["comment"] = new_comm
                    save_config(cfg)
                    Prompt.ask("[bold green]● Description updated! Press Enter...[/]")
        elif choice == "d":
            if not entries:
                continue
            idx_in = Prompt.ask(f"Enter number to delete [1-{len(entries)}]").strip()
            if idx_in.isdigit() and 1 <= int(idx_in) <= len(entries):
                del_item = entries.pop(int(idx_in)-1)
                save_config(cfg)
                apply_firewall(cfg)
                Prompt.ask(f"[bold green]● Removed {del_item.get('country')} from Global Blacklist! Press Enter...[/]")

def manage_group_geoip_tui(cfg, gkey, gname, ports_desc):
    while True:
        os.system("clear")
        gdata = cfg.setdefault("geoip", {}).setdefault("groups", {}).setdefault(gkey, {"mode": "disabled", "countries": []})
        mode = gdata.get("mode", "disabled").lower()
        mode_styled = "[bold green]WHITELIST[/]" if mode == "whitelist" else ("[bold red]BLACKLIST[/]" if mode == "blacklist" else "[bold yellow]DISABLED[/]")

        header_text = Text()
        header_text.append(f"● GEOIP POLICY FOR: {gname.upper()}\n", style="bold cyan")
        header_text.append(f"Target Ports: {ports_desc}  •  Current Mode: {mode.upper()}", style="dim white")
        console.print(Panel(Align.center(header_text), box=box.ROUNDED, border_style="cyan"))

        t = Table(box=box.ROUNDED, header_style="bold cyan", border_style="cyan", expand=True)
        t.add_column("[ # ]", justify="center", style="bold cyan", width=7, min_width=7, max_width=7, no_wrap=True)
        t.add_column("Country / Code", style="bold white", width=22, min_width=22, max_width=22, no_wrap=True)
        t.add_column("Policy", style="bold green" if mode=="whitelist" else "bold red", width=26, min_width=26, max_width=26, no_wrap=True)
        t.add_column("Description / Label", style="italic yellow", ratio=1)

        entries = gdata.setdefault("countries", [])
        if not entries:
            t.add_row("[ - ]", "No countries configured", mode_styled, "-")
        else:
            for idx, item in enumerate(entries, 1):
                cc = item.get("country", "").upper()
                cname = get_country_name(cc)
                pol_str = "ALLOW (Whitelist)" if mode == "whitelist" else ("BLOCK (Blacklist)" if mode == "blacklist" else "INACTIVE")
                t.add_row(f"[ {idx:2d} ]", f"{cc} ({cname})", pol_str, item.get("comment", ""))

        console.print(t)

        action_table = Table(box=box.SIMPLE, show_header=False, expand=True)
        action_table.add_column("Key", style="bold cyan", width=10, justify="center")
        action_table.add_column("Action", style="white")
        action_table.add_row("[ M ]", f"● Change Mode (Current: {mode_styled} -> Whitelist / Blacklist / Disabled)")
        action_table.add_row("[ A ]", "● Add Country to this Group Filter (e.g. UA, US, DE)")
        action_table.add_row("[ D ]", "● Delete Country from this Group Filter")
        action_table.add_row("[ B ]", "● Back to GeoIP Menu")
        console.print(Panel(action_table, title="[bold white]● Available Actions[/]", box=box.ROUNDED, border_style="dim white"))

        choice = Prompt.ask("[bold green]Choose action [M/A/D/B][/]", choices=["m", "a", "d", "b", "q", ""], default="b").strip().lower()

        if choice in ["b", "q", ""]:
            break
        elif choice == "m":
            new_mode = Prompt.ask("Select mode", choices=["whitelist", "blacklist", "disabled", "w", "b", "d"], default=mode).strip().lower()
            mode_map = {"w": "whitelist", "b": "blacklist", "d": "disabled", "whitelist": "whitelist", "blacklist": "blacklist", "disabled": "disabled"}
            gdata["mode"] = mode_map.get(new_mode, "disabled")
            save_config(cfg)
            apply_firewall(cfg)
            Prompt.ask(f"[bold green]● Mode set to {gdata['mode'].upper()}! Press Enter...[/]")
        elif choice == "a":
            cc_in = Prompt.ask("Enter 2-letter Country Code (e.g. UA, US, DE, PL, GB)").strip().upper()
            if len(cc_in) != 2 or not cc_in.isalpha():
                console.print("[bold red]Invalid country code. Must be 2 letters (e.g. UA).[/]")
                Prompt.ask("Press Enter...")
                continue
            cname = get_country_name(cc_in)
            comm = Prompt.ask(f"Enter description (Country: {cname})", default=cname).strip()

            console.print(f"[cyan]Fetching CIDRs for {cc_in} ({cname})...[/]")
            fetch_country_zone(cc_in)
            v4, v6 = get_country_cidrs(cc_in)
            console.print(f"[green]✔ Cached {len(v4)} IPv4 CIDRs and {len(v6)} IPv6 CIDRs for {cc_in}.[/]")

            found = False
            for it in entries:
                if it.get("country", "").upper() == cc_in:
                    it["comment"] = comm
                    found = True
                    break
            if not found:
                entries.append({"country": cc_in, "comment": comm})

            if gdata.get("mode") == "disabled":
                if Confirm.ask("Group filter mode is currently DISABLED. Enable WHITELIST mode now?", default=True):
                    gdata["mode"] = "whitelist"

            save_config(cfg)
            apply_firewall(cfg)
            Prompt.ask("[bold green]● Country added and firewall updated! Press Enter...[/]")
        elif choice == "d":
            if not entries:
                continue
            idx_in = Prompt.ask(f"Enter number to delete [1-{len(entries)}]").strip()
            if idx_in.isdigit() and 1 <= int(idx_in) <= len(entries):
                del_item = entries.pop(int(idx_in)-1)
                save_config(cfg)
                apply_firewall(cfg)
                Prompt.ask(f"[bold green]● Removed {del_item.get('country')}! Press Enter...[/]")

def manage_geoip_menu_tui(cfg):
    while True:
        os.system("clear")
        header_text = Text()
        header_text.append("● GEOIP & COUNTRY-BASED ACCESS CONTROL MANAGER\n", style="bold cyan")
        header_text.append("Configure Global Country Bans & Per-Service Country Whitelists / Blacklists", style="dim white")
        console.print(Panel(Align.center(header_text), box=box.ROUNDED, border_style="cyan"))

        menu_table = Table(box=box.ROUNDED, show_header=False, expand=True, border_style="cyan")
        menu_table.add_column("Key", style="bold cyan", width=8, justify="center")
        menu_table.add_column("GeoIP Category / Service", style="bold white", width=36)
        menu_table.add_column("Scope / Details", style="dim white")

        num_gbl = len(cfg.get("geoip", {}).get("global_blacklist", []))
        menu_table.add_row("[ 1 ]", "● Global Country Blacklist", f"Unconditional drop for entire server ({num_gbl} blocked)")
        
        http_mode = cfg.get("geoip", {}).get("groups", {}).get("http_only", {}).get("mode", "disabled").upper()
        menu_table.add_row("[ 2 ]", "● HTTP / HTTPS Web (80, 443)", f"Web portal country filtering (Mode: {http_mode})")
        
        mail_mode = cfg.get("geoip", {}).get("groups", {}).get("mail_clients", {}).get("mode", "disabled").upper()
        menu_table.add_row("[ 3 ]", "● Mail Clients & Submission", f"POP3/IMAP/587 country filtering (Mode: {mail_mode})")
        
        pub_mode = cfg.get("geoip", {}).get("groups", {}).get("public_ports", {}).get("mode", "disabled").upper()
        menu_table.add_row("[ 4 ]", "● Public World Ports (25 SMTP)", f"Port 25 SMTP country filtering (Mode: {pub_mode})")
        
        trust_mode = cfg.get("geoip", {}).get("groups", {}).get("trusted", {}).get("mode", "disabled").upper()
        menu_table.add_row("[ 5 ]", "● Trusted Services (3306, 10050)", f"MariaDB & Zabbix country filtering (Mode: {trust_mode})")
        
        menu_table.add_row("", "", "")
        menu_table.add_row("[ 6 ]", "● Update / Refresh GeoIP Database", "Download latest country CIDR zone files now")
        menu_table.add_row("", "", "")
        menu_table.add_row("[ 0 ]", "● Back to Main Menu", "Return to primary firewall management console")

        console.print(Panel(menu_table, title="[bold white]● GeoIP Submenu[/]", box=box.ROUNDED, border_style="cyan"))

        choice = Prompt.ask("[bold green]Choose option [0-6][/]", choices=["0", "1", "2", "3", "4", "5", "6", "q", "exit", ""]).strip()

        if choice in ["0", "q", "exit", ""]:
            break
        elif choice == "1":
            manage_global_blacklist_tui(cfg)
        elif choice == "2":
            manage_group_geoip_tui(cfg, "http_only", "HTTP / HTTPS Web Access", "80/tcp, 443/tcp")
        elif choice == "3":
            manage_group_geoip_tui(cfg, "mail_clients", "Mail Clients & Submission", "110, 143, 587, 993, 995/tcp")
        elif choice == "4":
            manage_group_geoip_tui(cfg, "public_ports", "Public Ports (World)", "25/tcp SMTP")
        elif choice == "5":
            manage_group_geoip_tui(cfg, "trusted", "Trusted Services", "3306/tcp, 10050/tcp")
        elif choice == "6":
            update_all_geoip_caches(cfg)
            apply_firewall(cfg)
            Prompt.ask("[bold green]● GeoIP zones refreshed and firewall reloaded! Press Enter...[/]")

# ----------------- Interactive Main TUI -----------------
def interactive_tui():
    cfg = load_config()
    while True:
        os.system("clear")
        show_status(cfg)

        menu_table = Table(box=box.ROUNDED, show_header=False, expand=True, border_style="cyan")
        menu_table.add_column("Key", style="bold cyan", width=8, justify="center")
        menu_table.add_column("Category / Action", style="bold white", width=36)
        menu_table.add_column("Scope / Details", style="dim white")

        menu_table.add_row("[ 1 ]", "● Manage Admins Group", "Full access to ALL ports and protocols")
        menu_table.add_row("[ 2 ]", "● Manage Trusted Services", "Access to MariaDB (3306) and Zabbix-Agent (10050)")
        menu_table.add_row("[ 3 ]", "● Manage HTTP / HTTPS Web", "Access to Web Management Portal (Ports 80 & 443)")
        menu_table.add_row("[ 4 ]", "● Manage Mail Clients & Submission", "Access to POP3/IMAP/Submission (110, 143, 587, 993, 995)")
        menu_table.add_row("[ 5 ]", "● Manage Public World Ports", "Ports open to 0.0.0.0/0 (Port 25/tcp SMTP)")
        menu_table.add_row("[ 6 ]", "● Manage GeoIP & Country ACL", "Global Blacklist & Per-Group Country Filters (WL/BL)")
        menu_table.add_row("", "", "")
        menu_table.add_row("[ 7 ]", "● View Full Rules Table", "Detailed listing of all active ACLs and comments")
        menu_table.add_row("[ 8 ]", "● Apply Rules & Reload Now", "Regenerate and reload native NFTables rules")
        menu_table.add_row("[ 9 ]", "● Update GeoIP Database Now", "Download latest country CIDR zone files")
        
        mode_label = "Disable Protection (Open Mode)" if cfg.get("enabled", True) else "Enable Protection (Restricted ACL)"
        mode_color = "yellow" if cfg.get("enabled", True) else "green"
        menu_table.add_row("[ 10 ]", f"[{mode_color}]● {mode_label}[/]", "Toggle global firewall restriction mode")
        menu_table.add_row("", "", "")
        menu_table.add_row("[ 0 ]", "● Exit to Configuration Console", "Return to main eFa-Configure menu")

        console.print(Panel(menu_table, title="[bold white]● Main Menu Navigation[/]", box=box.ROUNDED, border_style="cyan"))

        choice = Prompt.ask("[bold green]Choose option [0-10][/]", choices=["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "q", "exit", ""]).strip()

        if choice in ["0", "q", "exit", ""]:
            break
        elif choice == "1":
            manage_group_tui(cfg, "admins", "● ADMINS ACCESS LIST (FULL PORTS)", "Hosts and subnets with unrestricted access to ALL TCP/UDP ports and protocols", header_style="bold magenta", border_style="magenta")
        elif choice == "2":
            manage_group_tui(cfg, "trusted", "● TRUSTED SERVICES ACCESS LIST", "Hosts allowed to connect to MariaDB (3306) and Zabbix-Agent (10050)", header_style="bold blue", border_style="blue")
        elif choice == "3":
            manage_group_tui(cfg, "http_only", "● HTTP / HTTPS WEB ACCESS LIST", "Hosts allowed to access Web Interface on HTTP (80) and HTTPS (443)", header_style="bold green", border_style="green")
        elif choice == "4":
            manage_group_tui(cfg, "mail_clients", "● MAIL CLIENTS & SUBMISSION ACCESS LIST", "Hosts allowed to connect to POP3 (110), IMAP (143), Submission (587), IMAPS (993), POP3S (995)", header_style="bold dark_cyan", border_style="dark_cyan")
        elif choice == "5":
            manage_group_tui(cfg, "public_ports", "● PUBLIC PORTS (WORLD OPEN)", "Ports open to the entire Internet (0.0.0.0/0), e.g. Port 25 SMTP", header_style="bold yellow", border_style="yellow")
        elif choice == "6":
            manage_geoip_menu_tui(cfg)
        elif choice == "7":
            os.system("clear")
            list_rules(cfg)
            Prompt.ask("[bold cyan]Press [Enter] to return to Main Menu...[/]")
        elif choice == "8":
            apply_firewall(cfg)
            Prompt.ask("[bold cyan]Press [Enter] to return to Main Menu...[/]")
        elif choice == "9":
            update_all_geoip_caches(cfg)
            apply_firewall(cfg)
            Prompt.ask("[bold cyan]Press [Enter] to return to Main Menu...[/]")
        elif choice == "10":
            cfg["enabled"] = not cfg.get("enabled", True)
            save_config(cfg)
            apply_firewall(cfg)
            status_str = "ENABLED (Restricted)" if cfg["enabled"] else "DISABLED (Open)"
            Prompt.ask(f"[bold green]● Protection mode set to {status_str}! Press [Enter] to continue...[/]")

def main():
    if os.geteuid() != 0:
        if RICH_AVAILABLE:
            console.print("[bold red]ERROR:[/] eFa-Firewall must be run as root.")
        else:
            print("[ERROR] eFa-Firewall must be run as root.")
        sys.exit(1)

    parser = argparse.ArgumentParser(description="EFA-NG Firewall & Access Control Manager (Native NFTables & GeoIP)")
    parser.add_argument("--apply", action="store_true", help="Generate and apply native NFTables rules from config")
    parser.add_argument("--status", action="store_true", help="Show current firewall status")
    parser.add_argument("--list", action="store_true", help="List all firewall rules, IPs, GeoIP and comments in rich table")
    parser.add_argument("--enable", action="store_true", help="Enable firewall restriction mode")
    parser.add_argument("--disable", action="store_true", help="Disable firewall restriction mode (open mode)")
    parser.add_argument("--add", nargs=3, metavar=("GROUP", "IP", "COMMENT"), help="Add IP to group (admins, trusted, http_only, mail_clients)")
    parser.add_argument("--del", dest="delete", nargs=2, metavar=("GROUP", "IP"), help="Delete IP from group")
    parser.add_argument("--geoip-block-add", nargs="+", metavar=("COUNTRY", "[COMMENT]"), help="Add country to Global Blacklist")
    parser.add_argument("--geoip-block-del", metavar="COUNTRY", help="Delete country from Global Blacklist")
    parser.add_argument("--update-geoip", action="store_true", help="Download/update all configured country CIDR zone files")
    parser.add_argument("--tui", action="store_true", help="Run interactive TUI console")

    args = parser.parse_args()
    cfg = load_config()

    if args.apply:
        success = apply_firewall(cfg)
        sys.exit(0 if success else 1)
    elif args.status:
        show_status(cfg)
        sys.exit(0)
    elif args.list:
        list_rules(cfg)
        sys.exit(0)
    elif args.enable:
        cfg["enabled"] = True
        save_config(cfg)
        apply_firewall(cfg)
        sys.exit(0)
    elif args.disable:
        cfg["enabled"] = False
        save_config(cfg)
        apply_firewall(cfg)
        sys.exit(0)
    elif args.update_geoip:
        update_all_geoip_caches(cfg)
        apply_firewall(cfg)
        sys.exit(0)
    elif args.add:
        group, ip, comment = args.add
        success = cli_add_entry(cfg, group, ip, comment)
        sys.exit(0 if success else 1)
    elif args.delete:
        group, ip = args.delete
        success = cli_del_entry(cfg, group, ip)
        sys.exit(0 if success else 1)
    elif args.geoip_block_add:
        cc = args.geoip_block_add[0].upper()
        comm = " ".join(args.geoip_block_add[1:]) if len(args.geoip_block_add) > 1 else get_country_name(cc)
        fetch_country_zone(cc)
        entries = cfg.setdefault("geoip", {}).setdefault("global_blacklist", [])
        found = False
        for it in entries:
            if it.get("country", "").upper() == cc:
                it["comment"] = comm
                found = True
                break
        if not found:
            entries.append({"country": cc, "comment": comm})
        save_config(cfg)
        apply_firewall(cfg)
        sys.exit(0)
    elif args.geoip_block_del:
        cc = args.geoip_block_del.upper()
        entries = cfg.get("geoip", {}).get("global_blacklist", [])
        cfg["geoip"]["global_blacklist"] = [it for it in entries if it.get("country", "").upper() != cc]
        save_config(cfg)
        apply_firewall(cfg)
        sys.exit(0)
    else:
        # Default action: run interactive TUI
        interactive_tui()

if __name__ == "__main__":
    main()
