import threading
import socket
import os
import io
import sys
import time

from typing import Tuple 

# os.system('clear')

from concurrent import futures
from datetime import datetime

# Remove warnings
# import os
# os.environ.setdefault("GRPC_VERBOSITY", "ERROR")
# os.environ.setdefault("GRPC_TRACE", "")

TUNNEL_REGISTRY: HostTunnelRegistry | None = None

import grpc

import traceback
import contextlib

from ..common.grpc import *
from .reservation import reservation_handler
from .rpc_manager import rpc_manager
from .acc_perms import perms
from ..common.utils import *
from .device_manager import get_all_devices, get_all_devices_str
from .user_group_cli import make_user_group_local, edit_user_group_local, list_user_groups_local, remove_user_group_local, make_enrollment_codes_local, remove_enrollment_code_local, list_enrollment_codes_local, export_user_groups_csv_local, export_enrollment_codes_csv_local
from .user_group_handler import user_group_handler, start_usergroup_cleanup_loop

from pathlib import Path


def _xdg_config_home() -> Path:
    return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))

def _cfg_dir() -> Path:
    # same convention as your serverrf CLI
    return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))

def _server_env_path() -> Path:
    return _cfg_dir() / "server.env"

def _read_env_kv(path: Path) -> dict[str, str]:
    out: dict[str, str] = {}
    if not path.exists():
        return out
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        out[k.strip()] = v.strip().strip('"').strip("'")
    return out

def _get_port(name: str, *, default: int | None = None) -> int:
    # precedence: real env var > server.env > default
    v = os.getenv(name)
    if v is None:
        kv = _read_env_kv(_server_env_path())
        v = kv.get(name)

    if v is None or str(v).strip() == "":
        if default is None:
            raise SystemExit(f"Missing {name}. Set it via serverrf --config or environment.")
        return int(default)

    try:
        n = int(str(v).strip())
    except Exception:
        raise SystemExit(f"Invalid {name} (expected int): {v!r}")

    if n <= 0 or n > 65535:
        raise SystemExit(f"{name} out of range (1..65535): {n}")
    return n

from ..host.host_tunnel_server import HostTunnelRegistry, HostTunnelServicer
from ..common.grpc import grpc_host_pb2_grpc as host_tunnel_pb2_grpc
from ..common.grpc import grpc_host_pb2 as host_tunnel_pb2

from pathlib import Path
from prompt_toolkit import PromptSession

session = PromptSession()

from dotenv import load_dotenv, find_dotenv
dotenv_path = find_dotenv('.env.server')
load_dotenv(dotenv_path)

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))

local_ip = s.getsockname()[0] # grab the local ip
# local_port = os.getenv('GRPC_PORT')
local_port = str(_get_port("GRPC_PORT"))    
cert_port = str(_get_port("CERT_PORT"))   

# keep dotenv for *other* settings if you want, but ports come from server.env
dotenv_path = find_dotenv('.env.server')
load_dotenv(dotenv_path)

# print(f"Local IP: {local_ip}")
# print(f"Local Port: {local_port}")

# Implement the GenericRPC service
class GenericRPCServicer(grpc_pb2_grpc.GenericRPCServicer):
    def Call(self, request, context):
        
        # Remote Admin
        # function_name_args = request.function_name.split(':')
        # if function_name_args[0] == "RemoteAdmin":
        #     ok, err, caller = verify_remote_admin(request.args)
        #     if not ok:
        #         # You can also: context.abort(grpc.StatusCode.PERMISSION_DENIED, err)
        #         return grpc_pb2.GenericRPCResponse(results={
        #             "Ok": map_arg(False),
        #             "Error": map_arg(err),
        #         })

        #     return grpc_pb2.GenericRPCResponse(
        #         results=remote_admin_call(function_name=function_name_args[1], args=request.args)
        #     )
            
        # Normal RPC
        response = rpc_manager.run_rpc(function_name=request.function_name, args=request.args)
        return grpc_pb2.GenericRPCResponse(results=response)
    

# Function to start the server
def serve():
    
    options = [
        ('grpc.max_send_message_length', 100 * 1024 * 1024),
        ('grpc.max_receive_message_length', 100 * 1024 * 1024)
    ]
    
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=100), options=options)
    grpc_pb2_grpc.add_GenericRPCServicer_to_server(GenericRPCServicer(), server)

    # For generating server credentials for secure connections
    
    # Rasp
    #* Server credentials generation: Add server ip to this line
    #* openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.1.169" -addext "subjectAltName=IP:192.168.1.169"
    
    #ubuntu VM
    # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.64.5" -addext "subjectAltName=IP:192.168.64.5"
    
    #router
    # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=128.97.88.21" -addext "subjectAltName=IP:128.97.88.21"

    #magikarp
    # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=192.168.1.109" -addext "subjectAltName=IP:192.168.1.109"
    
    #wlab
    # openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt -subj "/CN=164.67.195.207" -addext "subjectAltName=IP:164.67.195.207"

    # Read key and certificate
    
    # cert_key = Path(__file__).resolve().parent.parent/'certs'/'server.key'
    # cert_crt = Path(__file__).resolve().parent.parent/'certs'/'server.crt'
    
    # with cert_key.open('rb') as f:
    #     private_key = f.read()
    # with cert_crt.open('rb') as f:
    #     certificate_chain = f.read()
    
    # --- TLS certs live in user config (~/.config/remoterf/certs by default) ---
    xdg_config = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
    cert_dir = Path(os.getenv("REMOTERF_CERT_DIR", xdg_config / "remoterf" / "certs"))

    cert_key = cert_dir / "server.key"
    cert_crt = cert_dir / "server.crt"

    try:
        if not cert_key.exists() or not cert_crt.exists():
            missing = []
            if not cert_key.exists(): missing.append(str(cert_key))
            if not cert_crt.exists(): missing.append(str(cert_crt))
            raise FileNotFoundError("Missing TLS file(s):\n  " + "\n  ".join(missing))

        with cert_key.open("rb") as f:
            private_key = f.read()
        with cert_crt.open("rb") as f:
            certificate_chain = f.read()

    except Exception as e:
        printf("TLS certs/keys not found or unreadable.", Sty.RED)
        printf("Expected in: ", Sty.DEFAULT, f"{cert_dir}", Sty.MAGENTA)
        printf("Fix: run ", Sty.DEFAULT, "RRRFcerts --ip ", Sty.GREEN, f"{local_ip}", Sty.MAGENTA, " --force", Sty.DEFAULT)
        printf("Or set REMOTERF_CERT_DIR to your cert folder.", Sty.GRAY)
        print(f"\nDetails: {e}\n", file=sys.stderr)
        raise SystemExit(2)

    # Create server credentials
    server_credentials = grpc.ssl_server_credentials(
        ((private_key, certificate_chain,),))
    
    # --- attach HostTunnel to the SAME grpc server/port/creds ---
    directory_env = Path(__file__).resolve().parent.parent / "config" / ".env.host_directory"
    
        # --- attach HostTunnel to the SAME grpc server/port/creds ---
    global TUNNEL_REGISTRY
    TUNNEL_REGISTRY = HostTunnelRegistry()

    host_tunnel_pb2_grpc.add_HostTunnelServicer_to_server(
        HostTunnelServicer(TUNNEL_REGISTRY),
        server
    )

    # Inject registry into rpc_manager so it can forward
    # rpc_manager.set_tunnel_registry(registry)
    
    # Cert fetcher
    from .cert_provider import start_cert_provider
    cert_server, cert_thread = start_cert_provider(host=local_ip, port=int(cert_port))

    # Add secure port
    server.add_secure_port(f'{local_ip}:{local_port}', server_credentials)
    server.start()
    server.wait_for_termination()
    
def update_devices():
    while True:
        reservation_handler.update_devices()
        time.sleep(60)
        
threading.Thread(target=serve, daemon=True).start()
threading.Thread(target=update_devices, daemon=True).start()
start_usergroup_cleanup_loop(interval_sec=60)

start_time = datetime.now()

def welcome():
    printf(f"Server running nominally.", Sty.BOLD)
    printf(f"Local IP: ", Sty.DEFAULT, f"{local_ip}", Sty.ITALIC, f" | Port: ", Sty.DEFAULT, f"{local_port}", Sty.ITALIC)
    # printf(f"   -> Started at ", Sty.DEFAULT, f"{start_time}", Sty.DEFAULT)
    printf("Input ", Sty.DEFAULT, "'help'", Sty.BOLD," for help menu.", Sty.DEFAULT)

def clear():
    os.system('clear')
    welcome()
    
def set_acc():
    while True:
        username = session.prompt("Enter username to manage (c to cancel): ")
        if username == 'exit' or username == 'c' or username == 'e':
            return
        perm = perms.get_perms(username)
        if perm == []:
            printf(f"User: {username} does not exist.", Sty.RED)
        else:
            break
    
    print(f"User: {username} : <{perm[0][0]}>")
    printf(f"'s'", Sty.MAGENTA, " - Set perm details", Sty.DEFAULT)
    printf(f"'D'", Sty.MAGENTA, " - Delete user", Sty.DEFAULT)
    printf(f"'r'", Sty.MAGENTA, " - Manage reservations", Sty.DEFAULT)
    printf(f"'c'", Sty.MAGENTA, " - Cancel", Sty.DEFAULT)
    
    inpu = session.prompt(stylize("Enter command: ", Sty.DEFAULT))
    if inpu == 's':
        inpu2 = session.prompt(stylize("Setting perms: ", Sty.DEFAULT, " (U{User}/P{PowerUser}/A{Admin}) ", Sty.GRAY, ": ", Sty.DEFAULT))
        if inpu2 == 'U':
            perms.set_user(username)
            printf("Set to User. ", Sty.GREEN)
            
        elif inpu2 == 'P':
            printf("Setting PowerUser perms. ", Sty.BOLD, "Input 'c' to cancel anytime.", Sty.DEFAULT)
            
            while True: # Allowed Devices
                device_ids = session.prompt(stylize("Enter device ids allowed ", Sty.DEFAULT, 'int,int,int,...', Sty.GRAY, ": ", Sty.DEFAULT))
                if device_ids == 'c':
                    return
                try:
                    device_list = [int(x) for x in device_ids.split(',')]
                    if session.prompt(stylize(f"Confirm that this is the correct list of devices: {device_list} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':   
                        break
                except ValueError:
                    printf("Invalid input. Try Again.", Sty.RED)
                    
            while True: # Max Reservations
                max_res = session.prompt(stylize("Enter max reservations allowed ", Sty.DEFAULT, 'int', Sty.GRAY, ": ", Sty.DEFAULT))
                if max_res == 'c':
                    return
                try:
                    max_res = int(max_res)
                    if session.prompt(stylize(f"Confirm that this is the correct number of max reservations: {max_res} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
                        break
                except ValueError:
                    printf("Invalid input. Try Again.", Sty.RED)
                    
            while True: # Max Reservation Time
                max_res_time = session.prompt(stylize("Enter max length of a single reservation ", Sty.DEFAULT, 'int [seconds]', Sty.GRAY, ": ", Sty.DEFAULT))
                if max_res_time == 'c':
                    return
                try:
                    max_res_time = int(max_res_time)
                    if session.prompt(stylize(f"Confirm that this is the correct max reservation time in SECONDS: {max_res_time} ", Sty.DEFAULT, "(y/n)", Sty.GRAY, ": ", Sty.DEFAULT)) == 'y':
                        break
                except ValueError:
                    printf("Invalid input. Try Again.", Sty.RED)
            
            perms.set_poweruser(username, devices_allowed=device_list, max_reservations=max_res, max_reservation_time_sec=max_res_time)
        elif inpu2 == 'A':
            perms.set_admin(username)
        else:
            printf("Invalid input. Skipping.", Sty.RED)
        
    elif inpu == 'D':
        if input(f"Are you sure you want to delete user: {username}? (Y/N): ") == 'Y':
            printf(f"Deleting user: {username}", Sty.MAGENTA)
            reservation_handler.remove_user(username=username)
        else:
            printf("Delete canceled", Sty.GRAY)
    elif inpu == 'r':
        print("Managing reservations:")
        while True:
            inpu = session.prompt(stylize("Enter command: ", Sty.DEFAULT, "'a' to add, 'c' to cancel, 'e' to exit (anytime), 'v' to view. : ", Sty.GRAY))
            if inpu == 'e':
                break
            elif inpu == 'a':
                while True:
                    device_id = session.prompt(stylize("Enter device id: ", Sty.DEFAULT))
                    if device_id == 'c' or device_id == 'e':
                        break
                    try:
                        device_id = int(device_id)
                        if device_id not in get_all_devices():
                            printf("Invalid device id.", Sty.GRAY)
                            continue
                        start_time = session.prompt(stylize("Enter start time: ", Sty.DEFAULT, "YYYY-MM-DD HH:MM:SS -> ", Sty.GRAY))
                        end_time = session.prompt(stylize("Enter end time: ", Sty.DEFAULT, "YYYY-MM-DD HH:MM:SS -> ", Sty.GRAY))
                        token = reservation_handler.reserve_device(username=username, device_id=device_id, start_time=datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S'), end_time=datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S'), is_server=True)
                        api = unmap_arg(token['Token'])
                        printf("Reservation added. TOKEN -> ", Sty.GREEN, f"{api}", Sty.MAGENTA)
                        break
                    except ValueError:
                        printf("Invalid input. Try Again.", Sty.GRAY)
            elif inpu == 'v':
                reservations = reservation_handler.grab_all_reservations_server()
                for key, reservation in reservations.items():
                    if reservation[0] == username:
                        print(f"ID: {key}, User: {reservation[0]}, Device: {reservation[1]}, Start: {reservation[2]}, End: {reservation[3]}")
                if reservations == {}:
                    printf("No reservations found.", Sty.GRAY)
            elif inpu == 'c':
                while True:
                    try:
                        inpu2 = session.prompt(stylize("Enter reservation id to cancel: ", Sty.DEFAULT))
                        if inpu2 == 'c' or inpu2 == 'e':
                            break
                        reservation_handler.remove_reservation(res_id=int(inpu2))
                        printf(f"Reservation {inpu2} removed.", Sty.GREEN)
                        break
                    except Exception as e:
                        printf(f"Error: {e}", Sty.GRAY)
        
    elif inpu == 'c':
        print("Canceled. Exiting ...")

def get_devices():
    data = get_all_devices_str()
    
    printf("Devices:", Sty.BOLD)
    for key, value in data.items():
        printf(f"Device ID:", Sty.DEFAULT, f' {key}', Sty.MAGENTA, f" Device Name: ", Sty.DEFAULT, f"{(value)}", Sty.GRAY)

def _fmt_ms_ago(ms: int) -> str:
    if not ms:
        return "-"
    now = int(time.time() * 1000)
    dt = max(0, now - int(ms))
    if dt < 1000:
        return f"{dt}ms"
    if dt < 60_000:
        return f"{dt/1000:.1f}s"
    return f"{dt/60_000:.1f}m"

def _cmd_hosts_status() -> None:
    global TUNNEL_REGISTRY
    reg = TUNNEL_REGISTRY
    if reg is None:
        printf("Host tunnel registry not initialized yet.", Sty.RED)
        return

    hosts = reg.list_hosts()     # host_id -> HostStatus
    devices = reg.list_devices() # device_id -> DeviceStatus

    # Build host -> device list
    by_host: dict[str, list[str]] = {}
    for did, ds in devices.items():
        by_host.setdefault(ds.host_id or "-", []).append(did)

    printf("Hosts (live):", Sty.BOLD)
    if not hosts:
        printf("  (none)", Sty.GRAY)
        return

    for hid, hs in sorted(hosts.items(), key=lambda kv: kv[0]):
        dlist = sorted(by_host.get(hid, []))
        printf(
            "  ", Sty.DEFAULT,
            f"{hid}", Sty.MAGENTA,
            "  online=", Sty.DEFAULT, f"{hs.online}", Sty.GREEN if hs.online else Sty.RED,
            "  last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(hs.last_seen_ms)} ago", Sty.BLUE,
            "  devices=", Sty.DEFAULT, f"{len(dlist)}", Sty.GRAY
        )
        for did in dlist:
            ds = devices.get(did)
            if ds is None:
                continue
            printf(
                "     - ", Sty.DEFAULT,
                f"{did}", Sty.MAGENTA,
                "  local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
                "  online=", Sty.DEFAULT, f"{ds.online}", Sty.GREEN if ds.online else Sty.RED,
                "  last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.BLUE
            )

def _cmd_devices_status() -> None:
    global TUNNEL_REGISTRY
    reg = TUNNEL_REGISTRY
    if reg is None:
        printf("Host tunnel registry not initialized yet.", Sty.RED)
        return

    devices = reg.list_devices()
    printf("Devices (live):", Sty.BOLD)
    if not devices:
        printf("  (none)", Sty.GRAY)
        return

    for did, ds in sorted(devices.items(), key=lambda kv: kv[0]):
        printf(
            "  ", Sty.DEFAULT,
            f"{did}", Sty.MAGENTA,
            "  host=", Sty.DEFAULT, f"{ds.host_id or '-'}", Sty.GRAY,
            "  local_id=", Sty.DEFAULT, f"{ds.host_local_id}", Sty.GRAY,
            "  online=", Sty.DEFAULT, f"{ds.online}", Sty.GREEN if ds.online else Sty.RED,
            "  last_seen=", Sty.DEFAULT, f"{_fmt_ms_ago(ds.last_seen_ms)} ago", Sty.BLUE
        )

# def help():
#     printf("/------- HELP -------/", Sty.BOLD)
#     printf("'exit'          ", Sty.MAGENTA, " - Stop server.", Sty.DEFAULT)
#     printf("'help'          ", Sty.MAGENTA, " - Help.", Sty.DEFAULT)
#     printf("'clear'         ", Sty.MAGENTA, " - Clear terminal screen.", Sty.DEFAULT)
#     printf("'show a'         ", Sty.MAGENTA, " - Print all accounts", Sty.DEFAULT)
#     printf("'show d'         ", Sty.MAGENTA, " - Print all devices", Sty.DEFAULT)
#     # printf("'view p'        ", Sty.MAGENTA, " - Print all perms", Sty.DEFAULT) # Deprecated
#     printf("'show r'         ", Sty.MAGENTA, " - Print all reservations", Sty.DEFAULT)
#     printf("'rm aa'         ", Sty.MAGENTA, " - Remove all accounts", Sty.DEFAULT)
#     # printf("'rm a'          ", Sty.MAGENTA, " - Remove one account", Sty.DEFAULT)
#     printf("'rm ar'         ", Sty.MAGENTA, " - Remove all reservations", Sty.DEFAULT)
#     printf("'rm db'         ", Sty.MAGENTA, " - Remove all database entries", Sty.DEFAULT)
#     printf("'setacc'        ", Sty.MAGENTA, " - Set account details (and deletion)", Sty.DEFAULT)
    
#     printf("'mk group'      ", Sty.MAGENTA, " - Make a user group", Sty.DEFAULT)
#     printf("'rm group'      ", Sty.MAGENTA, " - Remove a user group", Sty.DEFAULT)
#     printf("'edit group'    ", Sty.MAGENTA, " - Edit a user group", Sty.DEFAULT)
#     printf("'show g'        ", Sty.MAGENTA, " - View all user groups", Sty.DEFAULT)
    
#     printf("'mk codes'      ", Sty.MAGENTA, " - Make enrollment codes", Sty.DEFAULT)
#     printf("'show c'        ", Sty.MAGENTA, " - View all enrollment codes", Sty.DEFAULT)
#     printf("'rm codes'      ", Sty.MAGENTA, " - Remove enrollment code", Sty.DEFAULT)
    
# def server_input(inpu:str) -> bool:
#     if inpu == "h" or inpu == "help":
#         help()
#     elif inpu == 'show r':
#         reservation_handler.print_all_reservations()
#     elif inpu == 'show a':
#         reservation_handler.print_all_accounts()
#     elif inpu == 'show p':
#         perms.print_perms()
#     elif inpu == 'show d':
#         get_devices()
#     elif inpu == 'clear':
#         clear()
#     elif inpu == 'rm aa':
#         reservation_handler.remove_all_users()
#     elif inpu == 'rm ar':
#         reservation_handler.remove_all_reservations()
#     elif inpu == 'rm db':
#         reservation_handler.rm_db()
#     elif inpu == 'setacc':
#         set_acc()
#     elif inpu in ('mk group', 'mk g'):
#         make_user_group_local(session)
#     elif inpu in ('rm group', 'rm g'):
#         remove_user_group_local(session)
#     elif inpu in ('edit group', 'edit g'):
#         edit_user_group_local(session)
#     elif inpu in ('show g', 'show groups'):
#         list_user_groups_local(session)
#     elif inpu in ('mk codes', 'mk code', 'mk c'):
#         make_enrollment_codes_local(session)
#     elif inpu == 'show c':
#         list_enrollment_codes_local(session)
#     elif inpu in ('rm code', 'rm c', 'rm codes'):
#         remove_enrollment_code_local(session)
#     elif inpu in ('exit', 'quit'):
#         return False
#     else:
#         print(f"Invalid command: {inpu}")
#     return True

from typing import Callable

def _norm_cmd(s: str) -> str:
    return " ".join((s or "").strip().split()).lower()

def _cmd_status() -> None:
    try:
        uptime = datetime.now() - start_time
        printf("Status:", Sty.BOLD)
        printf("  Started: ", Sty.DEFAULT, f"{start_time}", Sty.BLUE)
        printf("  Uptime : ", Sty.DEFAULT, f"{str(uptime).split('.')[0]}", Sty.BLUE)
        printf("  Bind   : ", Sty.DEFAULT, f"{local_ip}:{local_port}", Sty.MAGENTA)
        printf("  Cert   : ", Sty.DEFAULT, f"{local_ip}:{cert_port}", Sty.MAGENTA)
    except Exception:
        printf("Status unavailable.", Sty.RED)

def help() -> None:
    printf("RemoteRF Server Shell", Sty.BOLD)
    printf("Type ", Sty.DEFAULT, "help", Sty.MAGENTA, " to see commands. Type ", Sty.DEFAULT, "quit", Sty.MAGENTA, " to exit.", Sty.DEFAULT)
    print()

    printf("Server:", Sty.BOLD)
    printf("  help, h                  ", Sty.MAGENTA, "- Show this help", Sty.DEFAULT)
    printf("  clear                    ", Sty.MAGENTA, "- Clear the screen", Sty.DEFAULT)
    printf("  status                   ", Sty.MAGENTA, "- Show server status (uptime/bind)", Sty.DEFAULT)
    printf("  quit / exit              ", Sty.MAGENTA, "- Exit the shell", Sty.DEFAULT)
    print()

    printf("Users:", Sty.BOLD)
    printf("  users list               ", Sty.MAGENTA, "- List accounts", Sty.DEFAULT)
    printf("  users manage             ", Sty.MAGENTA, "- Manage a user (perms / delete / reservations)", Sty.DEFAULT)
    printf("  users purge              ", Sty.MAGENTA, "- Remove ALL users", Sty.DEFAULT)
    print()

    printf("Devices:", Sty.BOLD)
    printf("  devices list             ", Sty.MAGENTA, "- List devices", Sty.DEFAULT)
    print()

    printf("Reservations:", Sty.BOLD)
    printf("  reservations list        ", Sty.MAGENTA, "- List reservations", Sty.DEFAULT)
    printf("  reservations purge       ", Sty.MAGENTA, "- Remove ALL reservations", Sty.DEFAULT)
    print()

    printf("Groups:", Sty.BOLD)
    printf("  groups list              ", Sty.MAGENTA, "- List user groups", Sty.DEFAULT)
    printf("  groups create            ", Sty.MAGENTA, "- Create a user group (interactive)", Sty.DEFAULT)
    printf("  groups edit              ", Sty.MAGENTA, "- Edit a user group (interactive)", Sty.DEFAULT)
    printf("  groups delete            ", Sty.MAGENTA, "- Delete a user group (interactive)", Sty.DEFAULT)
    printf("  groups csv               ", Sty.MAGENTA, "- Export ALL user groups as CSV to Downloads", Sty.DEFAULT)
    print()

    printf("Enrollment Codes:", Sty.BOLD)
    printf("  codes list               ", Sty.MAGENTA, "- List enrollment codes", Sty.DEFAULT)
    printf("  codes create             ", Sty.MAGENTA, "- Create enrollment codes (interactive)", Sty.DEFAULT)
    printf("  codes delete             ", Sty.MAGENTA, "- Delete an enrollment code (interactive)", Sty.DEFAULT)
    printf("  codes csv                ", Sty.MAGENTA, "- Export ALL enrollment codes as CSV to Downloads", Sty.DEFAULT)
    print()
    
    printf("Host Tunnel (live):", Sty.BOLD)
    printf("  hosts status            ", Sty.MAGENTA, "- Live host online/last_seen/devices", Sty.DEFAULT)
    printf("  devices status          ", Sty.MAGENTA, "- Live device online/last_seen/route", Sty.DEFAULT)
    print()

    printf("Database:", Sty.BOLD)
    printf("  db purge                 ", Sty.MAGENTA, "- Remove all database entries", Sty.DEFAULT)
    print()

def server_input(inpu: str) -> bool:
    cmd = _norm_cmd(inpu)
    if not cmd:
        return True

    def _stay(fn: Callable[[], None]) -> bool:
        fn()
        return True

    DISPATCH: dict[str, Callable[[], bool]] = {
        # ---- server ----
        "help": lambda: _stay(help),
        "h": lambda: _stay(help),
        "?": lambda: _stay(help),

        "clear": lambda: _stay(clear),
        "cls": lambda: _stay(clear),

        "status": lambda: _stay(_cmd_status),
        "server status": lambda: _stay(_cmd_status),

        "quit": lambda: False,
        "exit": lambda: False,
        "q": lambda: False,

        # ---- users ----
        "users list": lambda: _stay(reservation_handler.print_all_accounts),
        "u list": lambda: _stay(reservation_handler.print_all_accounts),
        "accounts list": lambda: _stay(reservation_handler.print_all_accounts),

        "users manage": lambda: _stay(set_acc),
        "users manage": lambda: _stay(set_acc),
        "setacc": lambda: _stay(set_acc),  # old

        "users purge": lambda: _stay(reservation_handler.remove_all_users),
        "rm aa": lambda: _stay(reservation_handler.remove_all_users),  # old

        # perms (kept; optional to advertise)
        "users perms": lambda: _stay(perms.print_perms),
        "show p": lambda: _stay(perms.print_perms),  # old

        # ---- devices ----
        "devices list": lambda: _stay(get_devices),
        "show d": lambda: _stay(get_devices),  # old

        # ---- reservations ----
        "reservations list": lambda: _stay(reservation_handler.print_all_reservations),
        "show r": lambda: _stay(reservation_handler.print_all_reservations),  # old

        "reservations purge": lambda: _stay(reservation_handler.remove_all_reservations),
        "rm ar": lambda: _stay(reservation_handler.remove_all_reservations),  # old

        # ---- db ----
        "db purge": lambda: _stay(reservation_handler.rm_db),
        "rm db": lambda: _stay(reservation_handler.rm_db),  # old

        # ---- groups ----
        "groups create": lambda: _stay(lambda: make_user_group_local(session)),
        "mk group": lambda: _stay(lambda: make_user_group_local(session)),  # old
        "mk g": lambda: _stay(lambda: make_user_group_local(session)),      # old

        "groups delete": lambda: _stay(lambda: remove_user_group_local(session)),
        "rm group": lambda: _stay(lambda: remove_user_group_local(session)),  # old
        "rm g": lambda: _stay(lambda: remove_user_group_local(session)),      # old

        "groups edit": lambda: _stay(lambda: edit_user_group_local(session)),
        "edit group": lambda: _stay(lambda: edit_user_group_local(session)),  # old
        "edit g": lambda: _stay(lambda: edit_user_group_local(session)),      # old

        "groups list": lambda: _stay(lambda: list_user_groups_local(session)),
        "show g": lambda: _stay(lambda: list_user_groups_local(session)),      # old
        "show groups": lambda: _stay(lambda: list_user_groups_local(session)), # old
        "groups csv": lambda: _stay(export_user_groups_csv_local),

        # ---- enrollment codes ----
        "codes create": lambda: _stay(lambda: make_enrollment_codes_local(session)),
        "mk codes": lambda: _stay(lambda: make_enrollment_codes_local(session)),  # old
        "mk code": lambda: _stay(lambda: make_enrollment_codes_local(session)),   # old
        "mk c": lambda: _stay(lambda: make_enrollment_codes_local(session)),      # old

        "codes list": lambda: _stay(lambda: list_enrollment_codes_local(session)),
        "show c": lambda: _stay(lambda: list_enrollment_codes_local(session)),    # old

        "codes delete": lambda: _stay(lambda: remove_enrollment_code_local(session)),
        "rm codes": lambda: _stay(lambda: remove_enrollment_code_local(session)), # old
        "rm code": lambda: _stay(lambda: remove_enrollment_code_local(session)),  # old
        "rm c": lambda: _stay(lambda: remove_enrollment_code_local(session)),     # old
        
        # ---- host tunnel ----
        "hosts status": lambda: _stay(_cmd_hosts_status),
        "host status": lambda: _stay(_cmd_hosts_status),
        "tunnel hosts": lambda: _stay(_cmd_hosts_status),

        "devices status": lambda: _stay(_cmd_devices_status),
        "tunnel devices": lambda: _stay(_cmd_devices_status),
        
        "codes csv": lambda: _stay(export_enrollment_codes_csv_local),
    }

    fn = DISPATCH.get(cmd)
    if fn is None:
        printf("Unknown command: ", Sty.RED, f"{inpu}", Sty.DEFAULT)
        printf("Type ", Sty.DEFAULT, "help", Sty.MAGENTA, " to see commands.", Sty.DEFAULT)
        return True

    return fn()

# os.system('clear')
welcome()

def main():
    while server_input(session.prompt(stylize('server@remote_rf: ', Sty.BOLD))):
        pass