/*=====================================================================
 * hellgate_page.c v2 - hellmon page 4: HELLGATE (talks to hellgated)
 *
 * #include'd from hellmon.c
 * NOT a standalone TU: relies on hellmon's sbuf_t/sb_addf, the ctr
 * registry, color globals, g_interval, g_wan/g_lan, read_file_str().
 *
 * v2: hellmon and hellgate now TALK. Division of labor:
 *
 *   data plane (every tick, hot):   pinned BPF maps, read directly
 *       via raw bpf(2) syscalls - stats sum + rule enumeration.
 *       No IPC, no forks, no libbpf.
 *
 *   control plane (rare, cold):     /run/hellgate.sock line protocol
 *       to the hellgated daemon - status, attach/detach/move/seed,
 *       block/unblock, drop-port/undrop-port. The daemon is the
 *       single enforcement point for guard rails (dport 22, WG
 *       listen-ports), so every client gets the same "no".
 *
 * v1 forked `ip -d link` 1-2x per tick plus hellgate.sh/system() per
 * action. v2 forks NOTHING - at -i 2 that's ~43k fork/execs per day
 * gone, replaced by one persistent unix socket and a few syscalls.
 *
 * Keys (page 4, normal mode):
 *   tab      switch pane (prefixes <-> ports)
 *   j/k      move selection (arrows too)
 *   a        add prefix (typed CIDR)      f  add port (dropdown)
 *   x        delete selected entry
 *   i        NIC menu (move gate / repoint hellmon WAN/LAN)
 *   A        attach   D  detach   R  re-seed bogons
 * Modal: j/k + enter, esc cancels.
 *=====================================================================*/

#include <linux/bpf.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <poll.h>
#include <arpa/inet.h>

#define HG_PIN        "/sys/fs/bpf/hellgate"
#define HG_SOCK       "/run/hellgate.sock"
#define HG_MAX_PFX    256
#define HG_MAX_PORTS  64
#define HG_MAX_NICS   16
#define HG_MAX_GATES  8
#define HG_IN_MAX     48
#define HG_MSG_MAX    160
#define HG_RESP_MAX   4096
#define HG_TIMEOUT_MS 700

/* must mirror hellgate.bpf.c */
enum { HG_ST_PASS = 0, HG_ST_DROP_SRC, HG_ST_DROP_DPORT,
       HG_ST_PASS_NONIP, HG_ST_DROP_MALFORMED, HG_ST_MAX };

struct hg_lpm_key { __u32 prefixlen; __u32 addr; };   /* addr = BE */

/*----- raw bpf(2): the per-tick fast path ---------------------------*/
static int hg_bpf(int cmd, union bpf_attr *a, unsigned sz){
    return (int)syscall(__NR_bpf, cmd, a, sz);
}
static int hg_obj_get(const char *path){
    union bpf_attr a; memset(&a, 0, sizeof a);
    a.pathname = (__u64)(unsigned long)path;
    return hg_bpf(BPF_OBJ_GET, &a, sizeof a);
}
static int hg_map_lookup(int fd, const void *k, void *v){
    union bpf_attr a; memset(&a, 0, sizeof a);
    a.map_fd = (__u32)fd;
    a.key    = (__u64)(unsigned long)k;
    a.value  = (__u64)(unsigned long)v;
    return hg_bpf(BPF_MAP_LOOKUP_ELEM, &a, sizeof a);
}
static int hg_map_next(int fd, const void *k, void *next){
    union bpf_attr a; memset(&a, 0, sizeof a);
    a.map_fd   = (__u32)fd;
    a.key      = (__u64)(unsigned long)k;   /* NULL -> first key */
    a.next_key = (__u64)(unsigned long)next;
    return hg_bpf(BPF_MAP_GET_NEXT_KEY, &a, sizeof a);
}

/*----- state --------------------------------------------------------*/
static int g_hg_fd_drop  = -1, g_hg_fd_ports = -1, g_hg_fd_stats = -1;
static int g_hg_ncpu     = 0;
static int g_hgc         = -1;           /* socket to hellgated       */

enum { HGM_NORMAL = 0, HGM_IN_CIDR, HGM_IN_PORT, HGM_MENU_PORT,
       HGM_MENU_NIC, HGM_MENU_NICACT };
static int  g_hg_mode  = HGM_NORMAL;
static int  g_hg_pane  = 0;              /* 0 = prefixes, 1 = ports   */
static int  g_hg_sel   = 0;
static int  g_hg_msel  = 0;
static char g_hg_in[HG_IN_MAX];
static int  g_hg_inlen = 0;
static char g_hg_msg[HG_MSG_MAX];
static int  g_hg_force_redisc = 0;       /* main loop checks this     */

static struct hg_lpm_key g_hg_pfx[HG_MAX_PFX];
static int   g_hg_npfx = 0;
static __u16 g_hg_port[HG_MAX_PORTS];
static int   g_hg_nport = 0;

/* daemon-reported status, refreshed each tick */
static char g_hg_gif[IFNAME_LEN] = "?";
static int  g_hg_att = 0;
static char g_hg_attmode[16] = "-";
static int  g_hg_daemon_ok = 0;
typedef struct { char ifname[IFNAME_LEN]; char mode[16]; } hg_gate_t;
static hg_gate_t g_hg_gate[HG_MAX_GATES];
static int g_hg_ngate = 0;

typedef struct {
    char ifname[IFNAME_LEN];
    char driver[32];
    char state[16];
    long speed;
    int  has_gate;
} hg_nic_t;
static hg_nic_t g_hg_nic[HG_MAX_NICS];
static int g_hg_nnic = 0;
static int g_hg_nic_pick = 0;

static const struct { unsigned short port; const char *label; }
HG_PORT_PRESETS[] = {
    {   23, "telnet scan"  },
    {  445, "smb"          },
    { 3389, "rdp"          },
    { 1433, "mssql"        },
    {  161, "snmp scan"    },
    { 8291, "winbox scan"  },
    { 5060, "sip scan (safe: Telnyx trunk is registering)" },
    {    0, "custom port..." },
};
#define HG_NPRESET ((int)(sizeof HG_PORT_PRESETS / sizeof HG_PORT_PRESETS[0]))

static const char *HG_NIC_ACTIONS[] = {
    "move hellgate to this NIC",
    "set as hellmon WAN",
    "set as hellmon LAN",
    "cancel",
};
#define HG_NACT 4

static void hg_msgf(const char *fmt, ...){
    va_list ap; va_start(ap, fmt);
    vsnprintf(g_hg_msg, sizeof g_hg_msg, fmt, ap);
    va_end(ap);
}

/*----- socket client ------------------------------------------------*/
static void hg_disconnect(void){
    if (g_hgc >= 0) close(g_hgc);
    g_hgc = -1;
    g_hg_daemon_ok = 0;
}

static int hg_connect(void){
    if (g_hgc >= 0) return 0;
    int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (fd < 0) return -1;
    struct sockaddr_un sa; memset(&sa, 0, sizeof sa);
    sa.sun_family = AF_UNIX;
    snprintf(sa.sun_path, sizeof sa.sun_path, "%s", HG_SOCK);
    if (connect(fd, (struct sockaddr *)&sa, sizeof sa) < 0)
        { close(fd); return -1; }
    fcntl(fd, F_SETFL, O_NONBLOCK);
    g_hgc = fd;
    return 0;
}

/* send one command, collect data lines into out (may be NULL),
 * return 0 on OK, -1 on ERR (first ERR text -> g_hg_msg),
 * -2 on daemon unreachable / timeout */
static int hg_req(const char *cmd, char *out, size_t outsz){
    if (hg_connect()) return -2;
    char line[HG_IN_MAX + 16];
    int n = snprintf(line, sizeof line, "%s\n", cmd);
    if (write(g_hgc, line, (size_t)n) != n) {
        hg_disconnect();
        if (hg_connect() || write(g_hgc, line, (size_t)n) != n)
            { hg_disconnect(); return -2; }
    }
    static char rb[HG_RESP_MAX];
    int rl = 0;
    size_t outlen = 0;
    if (out && outsz) out[0] = 0;
    for (;;) {
        /* one full line available? */
        char *nl = memchr(rb, '\n', (size_t)rl);
        if (nl) {
            *nl = 0;
            int consumed = (int)(nl + 1 - rb);
            if (!strncmp(rb, "OK", 2)) {
                memmove(rb, nl + 1, (size_t)(rl - consumed));
                return 0;
            }
            if (!strncmp(rb, "ERR", 3)) {
                hg_msgf("%s", rb[3] == ' ' ? rb + 4 : rb);
                memmove(rb, nl + 1, (size_t)(rl - consumed));
                return -1;
            }
            if (out) {
                size_t l = strlen(rb);
                if (outlen + l + 2 < outsz) {
                    memcpy(out + outlen, rb, l);
                    outlen += l;
                    out[outlen++] = '\n';
                    out[outlen] = 0;
                }
            }
            memmove(rb, nl + 1, (size_t)(rl - consumed));
            rl -= consumed;
            continue;
        }
        struct pollfd pf = { .fd = g_hgc, .events = POLLIN };
        int pr = poll(&pf, 1, HG_TIMEOUT_MS);
        if (pr <= 0) { hg_disconnect(); return -2; }
        ssize_t r = read(g_hgc, rb + rl, sizeof rb - 1 - (size_t)rl);
        if (r <= 0)  { hg_disconnect(); return -2; }
        rl += (int)r;
        rb[rl] = 0;
        if (rl >= (int)sizeof rb - 1) { hg_disconnect(); return -2; }
    }
}

/* per-tick: refresh gate status from the daemon */
static void hg_refresh_status(void){
    char out[1024];
    g_hg_ngate = 0;
    int rc = hg_req("status", out, sizeof out);
    g_hg_daemon_ok = (rc != -2);
    if (rc != 0) return;
    char *save = NULL;
    for (char *ln = strtok_r(out, "\n", &save); ln;
         ln = strtok_r(NULL, "\n", &save)) {
        char a[64], b[32];
        int flag;
        if (sscanf(ln, "iface %63s", a) == 1)
            snprintf(g_hg_gif, sizeof g_hg_gif, "%.15s", a);
        else if (sscanf(ln, "attached %d %31s", &flag, b) == 2) {
            g_hg_att = flag;
            snprintf(g_hg_attmode, sizeof g_hg_attmode, "%.15s", b);
        } else if (sscanf(ln, "gate %63s %31s", a, b) == 2 &&
                   g_hg_ngate < HG_MAX_GATES) {
            snprintf(g_hg_gate[g_hg_ngate].ifname,
                     sizeof g_hg_gate[0].ifname, "%.15s", a);
            snprintf(g_hg_gate[g_hg_ngate].mode,
                     sizeof g_hg_gate[0].mode, "%.15s", b);
            g_hg_ngate++;
        }
    }
}

static int hg_if_has_gate(const char *ifname){
    for (int i = 0; i < g_hg_ngate; i++)
        if (!strcmp(g_hg_gate[i].ifname, ifname)) return 1;
    return 0;
}

/*----- map fast path ------------------------------------------------*/
static int hg_possible_cpus(void){
    char buf[64];
    if (read_file_str("/sys/devices/system/cpu/possible", buf, sizeof buf) < 0)
        return (int)sysconf(_SC_NPROCESSORS_CONF);
    int lo = 0, hi = 0;
    if (sscanf(buf, "%d-%d", &lo, &hi) == 2) return hi - lo + 1;
    if (sscanf(buf, "%d", &lo) == 1)         return 1;
    return (int)sysconf(_SC_NPROCESSORS_CONF);
}

static void hg_close_maps(void){
    if (g_hg_fd_drop  >= 0) close(g_hg_fd_drop);
    if (g_hg_fd_ports >= 0) close(g_hg_fd_ports);
    if (g_hg_fd_stats >= 0) close(g_hg_fd_stats);
    g_hg_fd_drop = g_hg_fd_ports = g_hg_fd_stats = -1;
}

static void hg_open_maps(void){
    if (!g_hg_ncpu) g_hg_ncpu = hg_possible_cpus();
    if (g_hg_fd_drop  < 0) g_hg_fd_drop  = hg_obj_get(HG_PIN "/hellgate_drop4");
    if (g_hg_fd_ports < 0) g_hg_fd_ports = hg_obj_get(HG_PIN "/hellgate_dports");
    if (g_hg_fd_stats < 0) g_hg_fd_stats = hg_obj_get(HG_PIN "/hellgate_stats");
}

static void hg_load_prefixes(void){
    g_hg_npfx = 0;
    if (g_hg_fd_drop < 0) return;
    struct hg_lpm_key k, next;
    void *cur = NULL;
    while (g_hg_npfx < HG_MAX_PFX &&
           hg_map_next(g_hg_fd_drop, cur, &next) == 0) {
        k = next;
        g_hg_pfx[g_hg_npfx++] = k;
        cur = &k;
    }
}

static void hg_load_ports(void){
    g_hg_nport = 0;
    if (g_hg_fd_ports < 0) return;
    __u16 k, next;
    void *cur = NULL;
    while (g_hg_nport < HG_MAX_PORTS &&
           hg_map_next(g_hg_fd_ports, cur, &next) == 0) {
        k = next;
        g_hg_port[g_hg_nport++] = k;
        cur = &k;
    }
}

static int hg_read_stats(unsigned long long out[HG_ST_MAX]){
    if (g_hg_fd_stats < 0) return -1;
    static __u64 *buf = NULL;
    if (!buf) buf = calloc((size_t)g_hg_ncpu, sizeof *buf);   /* once */
    if (!buf) return -1;
    for (__u32 i = 0; i < HG_ST_MAX; i++) {
        out[i] = 0;
        if (hg_map_lookup(g_hg_fd_stats, &i, buf) != 0) {
            hg_close_maps();     /* stale fds after daemon re-pin */
            return -1;
        }
        for (int c = 0; c < g_hg_ncpu; c++) out[i] += buf[c];
    }
    return 0;
}

/*----- NIC scan (sysfs only, fork-free) -----------------------------*/
static int hg_ifname_ok(const char *s){
    if (!*s || strlen(s) >= IFNAME_LEN) return 0;
    for (; *s; s++)
        if (!(isalnum((unsigned char)*s) || *s=='_' || *s=='-' || *s=='.'))
            return 0;
    return 1;
}

static void hg_scan_nics(void){
    g_hg_nnic = 0;
    DIR *d = opendir("/sys/class/net");
    if (!d) return;
    struct dirent *de;
    while ((de = readdir(d)) && g_hg_nnic < HG_MAX_NICS) {
        if (de->d_name[0] == '.') continue;
        if (!hg_ifname_ok(de->d_name)) continue;
        char p[300], lnk[256];
        snprintf(p, sizeof p, "/sys/class/net/%s/device", de->d_name);
        if (access(p, F_OK) != 0) continue;      /* virtual: wg, vlan.. */
        hg_nic_t *nc = &g_hg_nic[g_hg_nnic];
        memset(nc, 0, sizeof *nc);
        snprintf(nc->ifname, sizeof nc->ifname, "%s", de->d_name);
        snprintf(p, sizeof p, "/sys/class/net/%s/device/driver", de->d_name);
        ssize_t l = readlink(p, lnk, sizeof lnk - 1);
        if (l > 0) {
            lnk[l] = 0;
            char *b = strrchr(lnk, '/');
            snprintf(nc->driver, sizeof nc->driver, "%.31s", b ? b + 1 : lnk);
        } else snprintf(nc->driver, sizeof nc->driver, "?");
        snprintf(p, sizeof p, "/sys/class/net/%s/operstate", de->d_name);
        if (read_file_str(p, nc->state, sizeof nc->state) < 0)
            snprintf(nc->state, sizeof nc->state, "?");
        snprintf(p, sizeof p, "/sys/class/net/%s/speed", de->d_name);
        char sb[32];
        nc->speed = read_file_str(p, sb, sizeof sb) == 0 ? atol(sb) : -1;
        nc->has_gate = hg_if_has_gate(nc->ifname);
        g_hg_nnic++;
    }
    closedir(d);
}

/*----- actions via the daemon ---------------------------------------*/
static void hg_action(const char *cmd, const char *okmsg){
    char out[512];
    int rc = hg_req(cmd, out, sizeof out);
    if (rc == 0)       hg_msgf("%s", okmsg);
    else if (rc == -2) hg_msgf("hellgated unreachable - service hellgate start");
    /* rc == -1: daemon's ERR text already in g_hg_msg */
    hg_refresh_status();
    hg_close_maps();             /* attach/move can re-pin: reopen */
}

static void hg_add_port(unsigned p){
    char cmd[48], ok[64];
    snprintf(cmd, sizeof cmd, "drop-port %u", p);
    snprintf(ok, sizeof ok, "dropping WAN dport %u at XDP", p);
    hg_action(cmd, ok);
}

/*----- render -------------------------------------------------------*/
static void hg_stat_line(sbuf_t *b, const char *lbl, const char *key,
                         unsigned long long v, int hot){
    ctr_t *c = ctr_get(key);
    if (!c->init) { c->base = c->prev = v; c->init = 1; }
    long long d = (long long)v - (long long)c->prev;
    c->cur = v;
    const char *col = d > 0 ? (hot ? RED : YEL) : DIM;
    sb_addf(b, "  %-14s %s%llu", lbl, col, v);
    if (d > 0) sb_addf(b, " (+%lld/%ds)", d, g_interval);
    sb_addf(b, "%s\n", RST);
    if (hot && d > 0) alert("hellgate %s +%lld", lbl, d);
}

static void hg_render(sbuf_t *b, double dt){
    (void)dt;
    hg_open_maps();
    hg_refresh_status();

    sb_addf(b, "%s=== HELLGATE: XDP early-drop gate ===%s", BOLD, RST);
    sb_addf(b, "  daemon: %s%s%s\n",
            g_hg_daemon_ok ? GRN : RED,
            g_hg_daemon_ok ? "up" : "DOWN", RST);
    if (!g_hg_daemon_ok)
        sb_addf(b, "%s(control offline - service hellgate start; pinned maps "
                   "still readable below)%s\n", YEL, RST);
    else if (g_hg_att)
        sb_addf(b, "gate: %sATTACHED%s (%s) on %s\n",
                GRN, RST, g_hg_attmode, g_hg_gif);
    else
        sb_addf(b, "gate: %sNOT ATTACHED%s on %s   (A = attach)\n",
                RED, RST, g_hg_gif);
    if (g_hg_att && !strcmp(g_hg_attmode, "xdpgeneric"))
        sb_addf(b, "%s!! generic mode: pre-skb benefit lost - check i40e/"
                   "queue setup%s\n", YEL, RST);

    unsigned long long st[HG_ST_MAX];
    if (hg_read_stats(st) == 0) {
        hg_stat_line(b, "passed",        "hg.pass",     st[HG_ST_PASS],           0);
        hg_stat_line(b, "passed non-ip", "hg.nonip",    st[HG_ST_PASS_NONIP],     0);
        hg_stat_line(b, "dropped src",   "hg.dropsrc",  st[HG_ST_DROP_SRC],       0);
        hg_stat_line(b, "dropped dport", "hg.dropport", st[HG_ST_DROP_DPORT],     0);
        hg_stat_line(b, "malformed",     "hg.malformed",st[HG_ST_DROP_MALFORMED], 1);
    } else {
        sb_addf(b, "%spinned maps not readable under %s%s\n",
                YEL, HG_PIN, RST);
    }

    hg_load_prefixes();
    hg_load_ports();
    if (g_hg_pane == 0 && g_hg_sel >= g_hg_npfx)  g_hg_sel = g_hg_npfx  ? g_hg_npfx  - 1 : 0;
    if (g_hg_pane == 1 && g_hg_sel >= g_hg_nport) g_hg_sel = g_hg_nport ? g_hg_nport - 1 : 0;

    sb_addf(b, "\n%s--- drop prefixes (%d)%s ---%s\n", BOLD, g_hg_npfx,
            g_hg_pane == 0 ? "  [active pane]" : "", RST);
    for (int i = 0; i < g_hg_npfx; i++) {
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &g_hg_pfx[i].addr, ip, sizeof ip);
        int cur = (g_hg_pane == 0 && i == g_hg_sel);
        sb_addf(b, " %s%s %s/%u%s\n", cur ? BOLD : DIM, cur ? ">" : " ",
                ip, g_hg_pfx[i].prefixlen, RST);
    }
    if (!g_hg_npfx)
        sb_addf(b, " %s(empty - R to seed bogons)%s\n", DIM, RST);

    sb_addf(b, "%s--- drop dports (%d)%s ---%s\n", BOLD, g_hg_nport,
            g_hg_pane == 1 ? "  [active pane]" : "", RST);
    for (int i = 0; i < g_hg_nport; i++) {
        const char *lbl = "";
        for (int j = 0; j < HG_NPRESET; j++)
            if (HG_PORT_PRESETS[j].port == g_hg_port[i])
                lbl = HG_PORT_PRESETS[j].label;
        int cur = (g_hg_pane == 1 && i == g_hg_sel);
        sb_addf(b, " %s%s %u %s%s\n", cur ? BOLD : DIM, cur ? ">" : " ",
                g_hg_port[i], lbl, RST);
    }
    if (!g_hg_nport)
        sb_addf(b, " %s(empty = dport gate off)%s\n", DIM, RST);

    sb_addf(b, "\n%stab=pane j/k=sel a=add-prefix f=add-port x=del "
               "i=NICs A=attach D=detach R=seed%s\n", DIM, RST);
    if (g_hg_msg[0])
        sb_addf(b, "%s>> %s%s\n", YEL, g_hg_msg, RST);

    /* modal overlays */
    if (g_hg_mode == HGM_IN_CIDR || g_hg_mode == HGM_IN_PORT) {
        sb_addf(b, "\n%s%s: %s_%s   (enter=ok esc=cancel)\n",
                BOLD, g_hg_mode == HGM_IN_CIDR ?
                "block prefix (a.b.c.d/len)" : "custom dport",
                g_hg_in, RST);
    } else if (g_hg_mode == HGM_MENU_PORT) {
        sb_addf(b, "\n%sdrop which dport? (j/k, enter, esc)%s\n", BOLD, RST);
        for (int i = 0; i < HG_NPRESET; i++) {
            int cur = (i == g_hg_msel);
            if (HG_PORT_PRESETS[i].port)
                sb_addf(b, " %s%s %5u  %s%s\n", cur ? BOLD : DIM,
                        cur ? ">" : " ", HG_PORT_PRESETS[i].port,
                        HG_PORT_PRESETS[i].label, RST);
            else
                sb_addf(b, " %s%s %s%s\n", cur ? BOLD : DIM,
                        cur ? ">" : " ", HG_PORT_PRESETS[i].label, RST);
        }
    } else if (g_hg_mode == HGM_MENU_NIC) {
        sb_addf(b, "\n%sNICs in this system (j/k, enter, esc)%s\n", BOLD, RST);
        for (int i = 0; i < g_hg_nnic; i++) {
            hg_nic_t *nc = &g_hg_nic[i];
            int cur = (i == g_hg_msel);
            sb_addf(b, " %s%s %-14s %-10s %-6s ", cur ? BOLD : DIM,
                    cur ? ">" : " ", nc->ifname, nc->driver, nc->state);
            if (nc->speed > 0) sb_addf(b, "%ldMb ", nc->speed);
            if (nc->has_gate)  sb_addf(b, "%s[gate]%s", GRN, RST);
            if (!strcmp(nc->ifname, g_wan)) sb_addf(b, "%s[hellmon:WAN]%s", YEL, RST);
            if (!strcmp(nc->ifname, g_lan)) sb_addf(b, "%s[hellmon:LAN]%s", YEL, RST);
            sb_addf(b, "%s\n", RST);
        }
        if (!g_hg_nnic) sb_addf(b, " %s(no physical NICs found?)%s\n", DIM, RST);
    } else if (g_hg_mode == HGM_MENU_NICACT) {
        sb_addf(b, "\n%s%s: (j/k, enter, esc)%s\n", BOLD,
                g_hg_nic[g_hg_nic_pick].ifname, RST);
        for (int i = 0; i < HG_NACT; i++)
            sb_addf(b, " %s%s %s%s\n", i == g_hg_msel ? BOLD : DIM,
                    i == g_hg_msel ? ">" : " ", HG_NIC_ACTIONS[i], RST);
    }
}

/*----- input --------------------------------------------------------*/
static void hg_commit_input(void){
    g_hg_in[g_hg_inlen] = 0;
    if (g_hg_mode == HGM_IN_CIDR) {
        char cmd[HG_IN_MAX + 8], ok[HG_IN_MAX + 32];
        snprintf(cmd, sizeof cmd, "block %s", g_hg_in);
        snprintf(ok, sizeof ok, "dropping src %s at XDP", g_hg_in);
        hg_action(cmd, ok);
    } else if (g_hg_mode == HGM_IN_PORT) {
        hg_add_port((unsigned)atoi(g_hg_in));
    }
    g_hg_mode = HGM_NORMAL;
    g_hg_inlen = 0; g_hg_in[0] = 0;
}

static void hg_delete_selected(void){
    char cmd[64], ok[80];
    if (g_hg_pane == 0) {
        if (g_hg_sel >= g_hg_npfx) { hg_msgf("nothing selected"); return; }
        struct hg_lpm_key k = g_hg_pfx[g_hg_sel];
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &k.addr, ip, sizeof ip);
        snprintf(cmd, sizeof cmd, "unblock %s/%u", ip, k.prefixlen);
        snprintf(ok,  sizeof ok,  "unblocked %s/%u", ip, k.prefixlen);
    } else {
        if (g_hg_sel >= g_hg_nport) { hg_msgf("nothing selected"); return; }
        snprintf(cmd, sizeof cmd, "undrop-port %u", g_hg_port[g_hg_sel]);
        snprintf(ok,  sizeof ok,  "no longer dropping dport %u",
                 g_hg_port[g_hg_sel]);
    }
    hg_action(cmd, ok);
}

/* returns 1 if consumed (caller re-renders + repaints) */
static int hg_handle_key(unsigned char c){
    if (g_hg_mode == HGM_IN_CIDR || g_hg_mode == HGM_IN_PORT) {
        if (c == 27)                     { g_hg_mode = HGM_NORMAL;
                                           g_hg_inlen = 0; g_hg_in[0] = 0; }
        else if (c == '\r' || c == '\n') hg_commit_input();
        else if (c == 127 || c == 8)     { if (g_hg_inlen) g_hg_in[--g_hg_inlen] = 0; }
        else if ((isdigit(c) || c == '.' || c == '/') &&
                 g_hg_inlen + 1 < HG_IN_MAX)
            { g_hg_in[g_hg_inlen++] = (char)c; g_hg_in[g_hg_inlen] = 0; }
        return 1;
    }
    if (g_hg_mode == HGM_MENU_PORT || g_hg_mode == HGM_MENU_NIC ||
        g_hg_mode == HGM_MENU_NICACT) {
        int nitems = g_hg_mode == HGM_MENU_PORT ? HG_NPRESET :
                     g_hg_mode == HGM_MENU_NIC  ? g_hg_nnic  : HG_NACT;
        if (c == 27 || c == 'q') { g_hg_mode = HGM_NORMAL; return 1; }
        if (c == 'j' && g_hg_msel + 1 < nitems) { g_hg_msel++; return 1; }
        if (c == 'k' && g_hg_msel > 0)          { g_hg_msel--; return 1; }
        if (c == '\r' || c == '\n') {
            if (g_hg_mode == HGM_MENU_PORT) {
                if (HG_PORT_PRESETS[g_hg_msel].port == 0) {
                    g_hg_mode = HGM_IN_PORT; g_hg_inlen = 0; g_hg_in[0] = 0;
                } else {
                    hg_add_port(HG_PORT_PRESETS[g_hg_msel].port);
                    g_hg_mode = HGM_NORMAL;
                }
            } else if (g_hg_mode == HGM_MENU_NIC) {
                if (!g_hg_nnic) { g_hg_mode = HGM_NORMAL; return 1; }
                g_hg_nic_pick = g_hg_msel;
                g_hg_msel = 0;
                g_hg_mode = HGM_MENU_NICACT;
            } else {
                hg_nic_t *nc = &g_hg_nic[g_hg_nic_pick];
                if (g_hg_msel == 0) {
                    char cmd[IFNAME_LEN + 8], ok[IFNAME_LEN + 24];
                    snprintf(cmd, sizeof cmd, "move %s", nc->ifname);
                    snprintf(ok,  sizeof ok,  "gate moved to %s", nc->ifname);
                    hg_action(cmd, ok);
                } else if (g_hg_msel == 1) {
                    g_wan = strdup(nc->ifname);
                    g_hg_force_redisc = 1;
                    hg_msgf("hellmon WAN -> %s (rediscovering)", nc->ifname);
                } else if (g_hg_msel == 2) {
                    g_lan = strdup(nc->ifname);
                    g_hg_force_redisc = 1;
                    hg_msgf("hellmon LAN -> %s (rediscovering)", nc->ifname);
                }
                g_hg_mode = HGM_NORMAL;
            }
            return 1;
        }
        return 1;   /* eat strays: menus feel modal */
    }

    switch (c) {
    case '\t': g_hg_pane = !g_hg_pane; g_hg_sel = 0;             return 1;
    case 'j':  g_hg_sel++;                                       return 1;
    case 'k':  if (g_hg_sel > 0) g_hg_sel--;                     return 1;
    case 'a':  g_hg_mode = HGM_IN_CIDR;
               g_hg_inlen = 0; g_hg_in[0] = 0;                   return 1;
    case 'f':  g_hg_mode = HGM_MENU_PORT; g_hg_msel = 0;         return 1;
    case 'x':  hg_delete_selected();                             return 1;
    case 'i':  hg_refresh_status(); hg_scan_nics();
               g_hg_mode = HGM_MENU_NIC; g_hg_msel = 0;          return 1;
    case 'A':  hg_action("attach", "attached");                  return 1;
    case 'D':  hg_action("detach", "detached");                  return 1;
    case 'R':  hg_action("seed",   "bogons seeded");             return 1;
    default:                                                     return 0;
    }
}
