/*
 * TermTCP CLI - Terminal TCP Client
 *
 * Layout:
 *   [status bar                           ]
 *   [output window  - session data, scrolls]
 *   [--- Monitor ---------------------------]  <- toggleable
 *   [monitor window - BPQ RF frames, scrolls]
 *   [╔═ Input ══════════════════════════════╗]
 *   [║ > text wraps here                    ║]
 *   [╚══════════════════════════════════════╝]
 *
 * BPQ protocol: 0xFF = monitor frame start, 0xFE = end.
 * Auto-login:   sends username\rpassword\rBPQTermTCP\r on connect.
 * Menu:         Ctrl+A
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <time.h>
#include <locale.h>
#include <wchar.h>
#include <ncurses.h>

#ifndef PATH_MAX
#define PATH_MAX 4096
#endif

#define VERSION      "0.0.53"
#define MAX_HOSTS    4
#define INPUT_MAX    512
#define CONF_FILE    ".termtcprc"
#define INPUT_INNER  2                      /* content rows inside input box  */
#define INPUT_OUTER  (INPUT_INNER + 2)      /* total rows incl. border        */
#define SCRL_MAX     500                    /* scrollback ring buffer depth   */
#define SCRL_W       512                    /* max bytes per stored line      */
#define HIST_MAX     100                    /* input history ring depth       */

/* ── Color pairs ─────────────────────────────────────────── */
#define CP_DEFAULT  1
#define CP_RED      2
#define CP_GREEN    3
#define CP_YELLOW   4
#define CP_CYAN     5
#define CP_MAGENTA  6
#define CP_STATUS   7   /* white on blue  - status bar        */
#define CP_INPUT    8   /* black on white - input area        */
#define CP_SEP      9   /* cyan on black  - separators        */
#define CP_FKEY    10   /* blue on white  - fkey chip label   */

/* ── Host config ─────────────────────────────────────────── */
typedef struct {
    char host[100];
    char port[10];
    char username[80];
    char password[80];
    char name[50];
} HostEntry;

typedef struct { char data[SCRL_W]; int len; int base_attr; } ScrlLine;

/* ── Session ──────────────────────────────────────────────────
 * All per-connection state in one struct so the program can run
 * multiple simultaneous sessions (one per tab in the GUI, or one
 * in the CLI).  S always points to the currently active session. */
#define MAX_SESSIONS  4
#define SESSION_LABEL 32

typedef struct {
    int    active;                  /* slot is in use           */
    int    host_idx;                /* index into hosts[]       */
    char   label[SESSION_LABEL];   /* display name / tab title */

    /* Connection */
    int    sock;
    int    connected;
    time_t connect_time;

    /* Logging */
    int    logging;
    FILE  *log_file;

    /* BPQ monitor frame accumulator */
    int    in_mon_frame;
    char   mon_fbuf[4096];
    int    mon_flen;

    /* Port tracking */
    int    port_seen;
    char   ports_line_buf[256];
    int    ports_line_len;
    int    ports_parse_mode;
    int    ports_loaded;

    /* Output scrollback */
    ScrlLine out_sbuf[SCRL_MAX];
    int      out_sbuf_head, out_sbuf_count;
    int      out_scroll;
    char     out_part[SCRL_W];
    int      out_part_len, out_part_base, out_part_cur;

    /* Monitor scrollback */
    ScrlLine mon_sbuf[SCRL_MAX];
    int      mon_sbuf_head, mon_sbuf_count;
    int      mon_scroll;
    char     mon_part[SCRL_W];
    int      mon_part_len, mon_part_base, mon_part_cur;

    /* Input */
    char   input_buf[INPUT_MAX];
    int    input_len;

    /* Input history */
    char   hist_buf[HIST_MAX][INPUT_MAX];
    int    hist_head, hist_count, hist_pos;
    char   hist_save[INPUT_MAX];

} Session;

static Session  sessions[MAX_SESSIONS];
static Session *S;   /* active (displayed) session */

HostEntry hosts[MAX_HOSTS];
int num_hosts = 0;

/* ── Signal handling ─────────────────────────────────────── *
 * SIGINT (Ctrl+C) is trapped rather than left at its default
 * (immediate process kill) so the program can ask for confirmation
 * instead of dropping a live session/unsent form with no warning.
 * The handler only sets a flag - all actual UI/exit work happens
 * in interactive_loop(), since signal handlers must stay minimal
 * and ncurses calls are not safe to make from inside one. */
static volatile sig_atomic_t sigint_pending = 0;

static void on_sigint(int signum)
{
    (void)signum;
    sigint_pending = 1;
}

/* set while receiving data for a background session — suppresses live screen writes */
static int suppress_draw = 0;

/* ── BPQ / monitor state (shared across all sessions) ─────── */
int mon_visible  = 1;
int mon_tx       = 1;
int mon_com      = 1;
int mon_nodes    = 0;
int mon_colour   = 1;
int mon_ui_only  = 1;
int port_mask    = 0xFFFF;

char port_names[32][32];   /* port 1-32 names, loaded from config */

/* ── ncurses windows ─────────────────────────────────────── */
WINDOW *status_win = NULL;
WINDOW *out_win    = NULL;
WINDOW *sep_win    = NULL;   /* separator between monitor and output panes */
WINDOW *mon_win    = NULL;
WINDOW *sep2_win   = NULL;   /* separator above input area */
WINDOW *input_win  = NULL;
WINDOW *fkey_win   = NULL;   /* function key hint bar at bottom */

/* ── ICS-213 General Message ─────────────────────────────── */
#define ICS_SBLINE_MAX  120   /* "SB <addressee>@<distribution>" line          */
#define ICS_FIELD_MAX   80    /* To / From / Subject single-line fields        */
#define ICS_DT_MAX      40    /* Date/Time field                               */
#define ICS_MSG_ROWS    12    /* visible rows in the message textarea          */
#define ICS_MSG_COLS    80    /* wrap width sent to the BBS (also box width)   */
#define ICS_MSG_LINES   60    /* max stored logical lines in the message body  */
#define ICS_MSG_LINELEN 200   /* max chars per logical (unwrapped) line typed  */

typedef struct {
    char sb_line[ICS_SBLINE_MAX];   /* BBS command: "SB PKTNET@USA" — sent verbatim */
    char subject[ICS_FIELD_MAX];    /* BBS title prompt + ICS field 4 (shared)      */
    char incident[ICS_FIELD_MAX];   /* ICS field 1 — Incident Name (optional)       */
    char to[ICS_FIELD_MAX];         /* ICS field 2 — To (Name and Position)         */
    char from[ICS_FIELD_MAX];       /* ICS field 3 — From (Name and Position)       */
    char date[ICS_FIELD_MAX];       /* ICS field 5 — Date (UTC)                     */
    char time_z[ICS_FIELD_MAX];     /* ICS field 6 — Time UTC, Z appended on send   */
    char msg_lines[ICS_MSG_LINES][ICS_MSG_LINELEN]; /* ICS field 7 — Message body   */
    int  msg_nlines;
    char appr_name[ICS_FIELD_MAX];  /* ICS field 8 — Approved by: Name             */
    char appr_sig[ICS_FIELD_MAX];   /* ICS field 8 — Approved by: Signature        */
    char appr_pos[ICS_FIELD_MAX];   /* ICS field 8 — Approved by: Position/Title   */
} IcsMessage;

/* ── PKTNET Packet Check-In form (https://vden.org/pktnet/check_in.html) ─── */
#define CHKIN_FIELD_MAX  80
#define CHKIN_COMMENT_MAX 500   /* form's own stated limit                   */

static const char *chkin_type_opts[]    = { "EXERCISE", "REAL EVENT" };
static const int   chkin_type_n         = 2;

static const char *chkin_band_opts[]    = { "AXIP", "HF", "VHF", "UHF", "SHF" };
static const int   chkin_band_n         = 5;

static const char *chkin_session_opts[] = {
    "AXIP", "AX25 Packet", "Pactor", "Robust Packet",
    "Ardop", "VARA HF", "VARA FM", "Mesh"
};
static const int   chkin_session_n      = 8;

typedef struct {
    char sb_line[ICS_SBLINE_MAX];     /* defaults to "SB PKTNET@USA"          */
    char subject[CHKIN_FIELD_MAX];
    char agency[CHKIN_FIELD_MAX];     /* optional agency/group name           */
    char date_time[ICS_DT_MAX];       /* 1a */
    char to[CHKIN_FIELD_MAX];         /* 1b - fixed "PKTNET@USA" but editable  */
    char from[CHKIN_FIELD_MAX];       /* 1c */
    char contact_name[CHKIN_FIELD_MAX]; /* 1d */
    char operators[CHKIN_FIELD_MAX];  /* 1e */
    int  type_idx;                    /* 2a - index into chkin_type_opts      */
    int  band_idx;                    /* 2c - index into chkin_band_opts      */
    int  session_idx;                 /* 2d - index into chkin_session_opts   */
    char location[CHKIN_FIELD_MAX];   /* 3a */
    char grid[CHKIN_FIELD_MAX];       /* 3b */
    char comment_lines[ICS_MSG_LINES][ICS_MSG_LINELEN]; /* 4 - textarea-edited */
    int  comment_nlines;
} PktnetCheckin;

/* ── Forward declarations ────────────────────────────────── */
void draw_status(void);
void create_windows(void);
void append_mon(const char *text, int len, int attr);
void append_out(const char *text, int len, int attr);
void draw_input(void);
void send_trace_options(void);
void tcp_disconnect(void);
static int  session_count(void);
static int  session_index(void);
static void session_switch(int n);
static void session_next(void);
static void session_prev(void);
static void session_new(void);
static void session_close(void);
static void do_connect(int idx);

/* ═══════════════════════════════════════════════════════════
 * Config
 * ═══════════════════════════════════════════════════════════ */

/* Config file is ~/.BPQTermTCP.ini — same format as the GTK GUI uses.
 * Sections: [Session 1] for monitor/display settings, [Host1]-[Host4] for hosts.
 * Simple key=value parser; we only write what we read to avoid clobbering
 * unknown keys the GUI may have written. */

static char cfg_path[PATH_MAX];

static void cfg_path_init(void)
{
    if (cfg_path[0]) return;
    char *home = getenv("HOME");
    if (!home) home = ".";
    snprintf(cfg_path, PATH_MAX, "%s/.BPQTermTCP.ini", home);
}

/* Trim trailing whitespace/newline in place */
static void trim_nl(char *s)
{
    int n = (int)strlen(s);
    while (n > 0 && (s[n-1] == '\n' || s[n-1] == '\r' || s[n-1] == ' ')) s[--n] = '\0';
}

void load_config(void)
{
    cfg_path_init();
    FILE *fp = fopen(cfg_path, "r");
    if (!fp) {
        /* Try legacy ~/.termtcprc for migration */
        char legacy[PATH_MAX];
        char *home = getenv("HOME");
        if (!home) home = ".";
        snprintf(legacy, PATH_MAX, "%s/.termtcprc", home);
        fp = fopen(legacy, "r");
        if (!fp) return;
        /* Parse old simple format */
        char line[512];
        num_hosts = 0;
        while (fgets(line, sizeof(line), fp) && num_hosts < MAX_HOSTS) {
            if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
            HostEntry *h = &hosts[num_hosts];
            int n = sscanf(line, "%99s %9s %79s %79s %49[^\n]",
                           h->host, h->port, h->username, h->password, h->name);
            if (n < 2) continue;
            if (n == 3) { strncpy(h->name, h->username, 49); h->username[0] = h->password[0] = '\0'; }
            if (n == 4) { strncpy(h->name, h->password, 49); h->password[0] = '\0'; }
            num_hosts++;
        }
        fclose(fp);
        return;
    }

    char  line[512];
    char  section[64] = "";
    int   host_idx    = -1;
    num_hosts = 0;

    while (fgets(line, sizeof(line), fp)) {
        trim_nl(line);
        if (line[0] == '#' || line[0] == ';' || line[0] == '\0') continue;

        /* Section header */
        if (line[0] == '[') {
            char *e = strchr(line, ']');
            if (e) { *e = '\0'; strncpy(section, line+1, 63); section[63] = '\0'; }
            host_idx = -1;
            if (strncmp(section, "Host", 4) == 0) {
                int n = atoi(section + 4);
                if (n >= 1 && n <= MAX_HOSTS) {
                    host_idx = n - 1;
                    if (host_idx >= num_hosts) {
                        /* ensure slot exists */
                        while (num_hosts <= host_idx) {
                            memset(&hosts[num_hosts], 0, sizeof(HostEntry));
                            num_hosts++;
                        }
                    }
                }
            }
            continue;
        }

        /* key=value */
        char *eq = strchr(line, '=');
        if (!eq) continue;
        *eq = '\0';
        char *key = line;
        char *val = eq + 1;

        if (strcmp(section, "Session 1") == 0) {
            if      (strcmp(key,"MTX")         == 0) mon_tx      = atoi(val);
            else if (strcmp(key,"MCOM")        == 0) mon_com     = atoi(val);
            else if (strcmp(key,"MonNODES")    == 0) mon_nodes   = atoi(val);
            else if (strcmp(key,"MONColour")   == 0) mon_colour  = atoi(val);
            else if (strcmp(key,"PortMask")    == 0) port_mask   = atoi(val);
            else if (strcmp(key,"MUIONLY")     == 0) mon_ui_only = atoi(val);
            else if (strcmp(key,"MonPorts")    == 0) { /* informational */ }
            /* Port names: PortName1=VHF etc. */
            else if (strncmp(key,"PortName",8) == 0) {
                int p = atoi(key + 8);
                if (p >= 1 && p <= 32) strncpy(port_names[p-1], val, 31);
            }
        } else if (host_idx >= 0 && host_idx < MAX_HOSTS) {
            HostEntry *h = &hosts[host_idx];
            if      (strcmp(key,"Host")     == 0) strncpy(h->host,     val, 99);
            else if (strcmp(key,"Port")     == 0) strncpy(h->port,     val,  9);
            else if (strcmp(key,"UserName") == 0) strncpy(h->username, val, 79);
            else if (strcmp(key,"Password") == 0) strncpy(h->password, val, 79);
            else if (strcmp(key,"Path")     == 0) strncpy(h->name,     val, 49);
        }
    }
    fclose(fp);

    /* Remove trailing empty hosts */
    while (num_hosts > 0 && hosts[num_hosts-1].host[0] == '\0') num_hosts--;
}

void save_config(void)
{
    cfg_path_init();

    /* Read existing file so we preserve any keys we don't know about */
    FILE *fp = fopen(cfg_path, "w");
    if (!fp) return;

    fprintf(fp, "[Session 1]\n");
    fprintf(fp, "MTX=%d\n",       mon_tx);
    fprintf(fp, "MCOM=%d\n",      mon_com);
    fprintf(fp, "MonNODES=%d\n",  mon_nodes);
    fprintf(fp, "MONColour=%d\n", mon_colour);
    fprintf(fp, "PortMask=%d\n",  port_mask);
    fprintf(fp, "MUIONLY=%d\n",   mon_ui_only);
    fprintf(fp, "MonPorts=%d\n",  num_hosts > 0 ? 8 : 1);
    /* Save non-default port names */
    for (int i = 0; i < 32; i++) {
        char def[32]; snprintf(def, 32, "Port %d", i+1);
        if (strcmp(port_names[i], def) != 0)
            fprintf(fp, "PortName%d=%s\n", i+1, port_names[i]);
    }

    for (int i = 0; i < num_hosts; i++) {
        fprintf(fp, "\n[Host%d]\n", i+1);
        fprintf(fp, "Host=%s\n",     hosts[i].host);
        fprintf(fp, "Port=%s\n",     hosts[i].port);
        fprintf(fp, "UserName=%s\n", hosts[i].username);
        fprintf(fp, "Password=%s\n", hosts[i].password);
        fprintf(fp, "Path=%s\n",     hosts[i].name);
    }
    fclose(fp);
}

/* ═══════════════════════════════════════════════════════════
 * Logging
 * ═══════════════════════════════════════════════════════════ */

void open_log(const char *prefix)
{
    time_t     now = time(NULL);
    struct tm *t   = localtime(&now);
    char       filename[PATH_MAX];
    snprintf(filename, PATH_MAX, "%s_%04d%02d%02d_%02d%02d%02d.log",
             prefix, t->tm_year+1900, t->tm_mon+1, t->tm_mday,
             t->tm_hour, t->tm_min, t->tm_sec);
    S->log_file = fopen(filename, "w");
    if (S->log_file) S->logging = 1;
}

void close_log(void)
{
    if (S->log_file) { fclose(S->log_file); S->log_file = NULL; S->logging = 0; }
}

/* ═══════════════════════════════════════════════════════════
 * Network
 * ═══════════════════════════════════════════════════════════ */

int tcp_connect(const char *host, const char *port)
{
    struct addrinfo hints, *res = NULL;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if (getaddrinfo(host, port, &hints, &res) != 0 || !res) return -1;

    int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (fd < 0) { freeaddrinfo(res); return -1; }

    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    connect(fd, res->ai_addr, res->ai_addrlen);
    freeaddrinfo(res);

    fd_set wfds;
    struct timeval tv = {5, 0};
    FD_ZERO(&wfds); FD_SET(fd, &wfds);
    if (select(fd+1, NULL, &wfds, NULL, &tv) <= 0) { close(fd); return -1; }

    int err = 0; socklen_t elen = sizeof(err);
    getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &elen);
    if (err) { close(fd); return -1; }

    flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
    return fd;
}

int tcp_send(const char *data, int len)
{
    if (S->sock < 0 || len <= 0) return -1;
    if (S->logging && S->log_file) { fwrite(data, 1, len, S->log_file); fflush(S->log_file); }
    return send(S->sock, data, len, 0);
}

void tcp_disconnect(void)
{
    if (S->sock >= 0) { close(S->sock); S->sock = -1; }
    S->connected        = 0;
    S->connect_time     = 0;
    S->in_mon_frame     = 0;
    S->mon_flen         = 0;
    S->ports_parse_mode = 0;
    S->ports_line_len   = 0;
    S->ports_loaded     = 0;
    S->port_seen        = 0;
    for (int i = 0; i < 32; i++) snprintf(port_names[i], 32, "Port %d", i + 1);
    draw_status();
}

/* ═══════════════════════════════════════════════════════════
 * BPQ protocol
 * ═══════════════════════════════════════════════════════════ */

void send_trace_options(void)
{
    if (S->sock < 0) return;
    char buf[80];
    int  len = snprintf(buf, sizeof(buf),
                        "\\\\\\\\%x %x %x %x %x %x\r",
                        port_mask, mon_tx, mon_com,
                        mon_nodes, mon_colour, mon_ui_only);
    send(S->sock, buf, len, 0);
}

/* Same as send_trace_options but appends P8=1, which tells BPQ to call
 * SendPortsForMonitor() — returns port list as 0xFF 0xFF N|num desc|... */
static void send_trace_options_with_ports(void)
{
    if (S->sock < 0) return;
    char buf[80];
    int  len = snprintf(buf, sizeof(buf),
                        "\\\\\\\\%x %x %x %x %x %x 0 1\r",
                        port_mask, mon_tx, mon_com,
                        mon_nodes, mon_colour, mon_ui_only);
    send(S->sock, buf, len, 0);
}

/* Parse the 0xFF 0xFF port-info packet from SendPortsForMonitor.
 * Format (after both 0xFF bytes): "N|portnum desc|portnum desc|..."
 * N = total entries; portnum 0 = Mail Monitor (skipped). */
static void parse_portinfo(const char *data, int len)
{
    const char *p = data, *e = data + len;
    int nports = 0;
    while (p < e && *p >= '0' && *p <= '9') nports = nports * 10 + (*p++ - '0');
    if (p >= e || *p != '|') return;
    p++;
    for (int i = 0; i < nports && p < e; i++) {
        int n = 0;
        while (p < e && *p >= '0' && *p <= '9') n = n * 10 + (*p++ - '0');
        if (p >= e || *p != ' ') break;
        p++;
        const char *desc = p;
        while (p < e && *p != '|') p++;
        int dlen = (int)(p - desc);
        if (n >= 1 && n <= 32 && dlen > 0) {
            int copy = dlen < 31 ? dlen : 31;
            memcpy(port_names[n - 1], desc, copy);
            port_names[n - 1][copy] = '\0';
            S->port_seen |= (1u << (n - 1));
        }
        if (p < e && *p == '|') p++;
    }
    S->ports_loaded = 1;
}

static int bpq_color_attr(unsigned char color_byte)
{
    /* Map BPQ Colours[] index (color_byte - 10) to nearest terminal color.
     * Colours[]: 1-4 = dark blues, 5-20 = greens, 21-60 = reds, 61-100 = brighter reds */
    int idx = (int)color_byte - 10;
    if (idx <  0)  return COLOR_PAIR(CP_DEFAULT);
    if (idx <  4)  return COLOR_PAIR(CP_CYAN);           /* dark blues → cyan */
    if (idx < 20)  return COLOR_PAIR(CP_GREEN);          /* greens */
    if (idx < 40)  return COLOR_PAIR(CP_RED);            /* dark reds */
    if (idx < 60)  return COLOR_PAIR(CP_RED)   | A_BOLD; /* medium reds */
    if (idx < 80)  return COLOR_PAIR(CP_YELLOW);         /* bright reds → yellow so visible */
    return COLOR_PAIR(CP_MAGENTA);
}

/* Scan monitor frame text for "Port=N" and record N in S->port_seen bitmask.
 * BPQ monitor frames use "Port=N" format (e.g. "KB8PMY>APRS Port=2 <UI>").
 * Port names come from the PORTS session command, not from monitor frames. */
static void detect_port_number(const char *text, int len)
{
    /* Skip optional BPQ color prefix (0x1B + color_byte) */
    const char *p = text;
    if (len >= 2 && (unsigned char)p[0] == 0x1B) { p += 2; len -= 2; }

    for (int i = 0; i + 5 <= len; i++) {
        if ((p[i]=='P'||p[i]=='p') && (p[i+1]=='o'||p[i+1]=='O') &&
            (p[i+2]=='r'||p[i+2]=='R') && (p[i+3]=='t'||p[i+3]=='T') &&
            p[i+4]=='=') {
            int n = 0, j = i + 5;
            while (j < len && p[j] >= '0' && p[j] <= '9')
                n = n * 10 + (p[j++] - '0');
            if (n >= 1 && n <= 32)
                S->port_seen |= (1 << (n-1));
        }
    }
}

/* Parse one complete session output line for the PORTS command response.
 * Expects:  "} Ports"      → start of list
 *           "  N name..."  → port entry (leading 2 spaces + number + name) */
static void parse_session_line(const char *line, int len)
{
    /* Trim trailing whitespace */
    while (len > 0 && (unsigned char)line[len-1] <= ' ') len--;
    if (len == 0) return;

    if (!S->ports_parse_mode) {
        /* Detect "} Ports" header (case-insensitive) */
        for (int i = 0; i + 6 < len; i++) {
            if (line[i] == '}' && line[i+1] == ' ' &&
                (line[i+2]=='P'||line[i+2]=='p') &&
                (line[i+3]=='o'||line[i+3]=='O') &&
                (line[i+4]=='r'||line[i+4]=='R') &&
                (line[i+5]=='t'||line[i+5]=='T') &&
                (line[i+6]=='s'||line[i+6]=='S')) {
                S->ports_parse_mode = 1;
                return;
            }
        }
        return;
    }

    /* In ports mode: parse "  N name" lines */
    if (len >= 4 && line[0]==' ' && line[1]==' ' && line[2]>='1' && line[2]<='9') {
        int n = 0, j = 2;
        while (j < len && line[j] >= '0' && line[j] <= '9')
            n = n * 10 + (line[j++] - '0');
        if (j < len && line[j] == ' ') j++;
        /* Name: from j to len, trim trailing spaces */
        int ne = len;
        while (ne > j && (unsigned char)line[ne-1] <= ' ') ne--;
        int nlen = ne - j;
        if (n >= 1 && n <= 32 && nlen > 0) {
            int copy = nlen < 31 ? nlen : 31;
            memcpy(port_names[n-1], line + j, copy);
            port_names[n-1][copy] = '\0';
            S->port_seen |= (1 << (n-1));
        }
    } else {
        if (S->ports_parse_mode) S->ports_loaded = 1;  /* list ended cleanly */
        S->ports_parse_mode = 0;
    }
}

/* Feed one session output byte through the line accumulator. */
static void feed_session_byte(unsigned char c)
{
    if (c == '\r' || c == '\n') {
        if (S->ports_line_len > 0) {
            parse_session_line(S->ports_line_buf, S->ports_line_len);
            S->ports_line_len = 0;
        }
    } else if (c >= 32 && c < 128 && S->ports_line_len < 255) {
        S->ports_line_buf[S->ports_line_len++] = (char)c;
    }
}

void bpq_process(const char *buf, int len)
{
    const char *p   = buf;
    const char *end = buf + len;

    while (p < end) {
        if (!S->in_mon_frame) {
            const char *ff = memchr(p, 0xFF, end - p);
            if (!ff) {
                /* All remaining bytes are session output */
                for (const char *q = p; q < end; q++)
                    feed_session_byte((unsigned char)*q);
                append_out(p, (int)(end - p), COLOR_PAIR(CP_DEFAULT));
                break;
            }
            if (ff > p) {
                /* Session output before the next monitor frame */
                for (const char *q = p; q < ff; q++)
                    feed_session_byte((unsigned char)*q);
                append_out(p, (int)(ff - p), COLOR_PAIR(CP_DEFAULT));
            }
            /* 0xFF 0xFF = SendPortsForMonitor response; 0xFF 0x1B = monitor frame */
            p = ff + 1;
            if (p < end && (unsigned char)*p == 0xFF) {
                /* Port info packet — parse in place, consume to end of buffer */
                parse_portinfo(p + 1, (int)(end - p - 1));
                p = end;
            } else {
                S->in_mon_frame = 1;
                S->mon_flen     = 0;
            }
        } else {
            const char *fe      = memchr(p, 0xFE, end - p);
            int         seg_len = fe ? (int)(fe - p) : (int)(end - p);
            if (seg_len > 0) {
                detect_port_number(p, seg_len);
                /* Accumulate into the per-frame buffer */
                int room = (int)sizeof(S->mon_fbuf) - 1 - S->mon_flen;
                if (room > 0) {
                    int add = seg_len < room ? seg_len : room;
                    memcpy(S->mon_fbuf + S->mon_flen, p, add);
                    S->mon_flen += add;
                }
            }
            if (fe) {
                /* Frame complete: apply client-side NODES filter then display */
                if (S->mon_flen > 0) {
                    S->mon_fbuf[S->mon_flen] = '\0';
                    /* Detect NODES broadcast by AX.25 destination ">NODES" in header */
                    int is_nodes = 0;
                    for (int i = 0; i <= S->mon_flen - 6 && !is_nodes; i++)
                        if (memcmp(S->mon_fbuf + i, ">NODES", 6) == 0) is_nodes = 1;
                    if (mon_nodes || !is_nodes)
                        append_mon(S->mon_fbuf, S->mon_flen, COLOR_PAIR(CP_GREEN));
                }
                S->mon_flen     = 0;
                S->in_mon_frame = 0;
                p = fe + 1;
            } else {
                break;
            }
        }
    }

    if (S->logging && S->log_file) { fwrite(buf, 1, len, S->log_file); fflush(S->log_file); }
}

/* ═══════════════════════════════════════════════════════════
 * ncurses UI
 * ═══════════════════════════════════════════════════════════ */

void init_colors(void)
{
    if (!has_colors()) return;
    start_color();
    use_default_colors();
    init_pair(CP_DEFAULT, COLOR_WHITE,   COLOR_BLACK);
    init_pair(CP_RED,     COLOR_RED,     COLOR_BLACK);
    init_pair(CP_GREEN,   COLOR_GREEN,   COLOR_BLACK);
    init_pair(CP_YELLOW,  COLOR_YELLOW,  COLOR_BLACK);
    init_pair(CP_CYAN,    COLOR_CYAN,    COLOR_BLACK);
    init_pair(CP_MAGENTA, COLOR_MAGENTA, COLOR_BLACK);
    init_pair(CP_STATUS,  COLOR_WHITE,   COLOR_BLUE);
    init_pair(CP_INPUT,   COLOR_BLACK,   COLOR_WHITE);
    init_pair(CP_SEP,     COLOR_CYAN,    COLOR_BLACK);
    init_pair(CP_FKEY,    COLOR_BLUE,    COLOR_WHITE);
}


/* Draw a box using Unicode cchar_t — bypasses ACS so it works on any UTF-8 terminal */
static void draw_box(WINDOW *w)
{
    cchar_t vt, hz, ul, ur, ll, lr;
    setcchar(&vt, L"│", 0, 0, NULL);   /* │ */
    setcchar(&hz, L"─", 0, 0, NULL);   /* ─ */
    setcchar(&ul, L"┌", 0, 0, NULL);   /* ┌ */
    setcchar(&ur, L"┐", 0, 0, NULL);   /* ┐ */
    setcchar(&ll, L"└", 0, 0, NULL);   /* └ */
    setcchar(&lr, L"┘", 0, 0, NULL);   /* ┘ */
    wborder_set(w, &vt, &vt, &hz, &hz, &ul, &ur, &ll, &lr);
}

/* Fill a 1-row window with plain '-' dashes and an optional centred label.
 * No ACS — works on any terminal. */
static void fill_sep(WINDOW *w, const char *label)
{
    if (!w) return;
    werase(w);
    wbkgd(w, COLOR_PAIR(CP_SEP));
    wattron(w, COLOR_PAIR(CP_SEP));
    cchar_t hz;
    setcchar(&hz, L"─", 0, 0, NULL);
    whline_set(w, &hz, COLS);
    if (label && COLS > 4) {
        int llen = (int)strlen(label);
        int col  = (COLS - llen) / 2;
        if (col < 0) col = 0;
        wattron(w, A_BOLD);
        mvwaddstr(w, 0, col, label);
        wattroff(w, A_BOLD);
    }
    wattroff(w, COLOR_PAIR(CP_SEP));
    wrefresh(w);
}

void create_windows(void)
{
    if (status_win) { delwin(status_win); status_win = NULL; }
    if (out_win)    { delwin(out_win);    out_win    = NULL; }
    if (sep_win)    { delwin(sep_win);    sep_win    = NULL; }
    if (mon_win)    { delwin(mon_win);    mon_win    = NULL; }
    if (sep2_win)   { delwin(sep2_win);   sep2_win   = NULL; }
    if (input_win)  { delwin(input_win);  input_win  = NULL; }
    if (fkey_win)   { delwin(fkey_win);   fkey_win   = NULL; }

    /* overhead = status(1) + sep2(1) + input(INPUT_OUTER) + fkey(1)
     *          + sep(1) if monitor visible                           */
    int overhead     = 1 + 1 + INPUT_OUTER + 1 + (mon_visible ? 1 : 0);
    int content_rows = LINES - overhead;
    if (content_rows < 4) content_rows = 4;

    int row = 0;

    /* Status bar */
    status_win = newwin(1, COLS, row, 0);
    wbkgd(status_win, COLOR_PAIR(CP_STATUS));
    leaveok(status_win, TRUE);
    row++;

    if (mon_visible) {
        int mon_h = content_rows / 2;
        int out_h = content_rows - mon_h;

        mon_win = newwin(mon_h, COLS, row, 0);
        wbkgd(mon_win, COLOR_PAIR(CP_DEFAULT));
        scrollok(mon_win, TRUE);
        leaveok(mon_win, TRUE);
        row += mon_h;

        sep_win = newwin(1, COLS, row, 0);
        leaveok(sep_win, TRUE);
        fill_sep(sep_win, " Output ");
        row++;

        out_win = newwin(out_h, COLS, row, 0);
        wbkgd(out_win, COLOR_PAIR(CP_DEFAULT));
        scrollok(out_win, TRUE);
        leaveok(out_win, TRUE);
        row += out_h;
    } else {
        out_win = newwin(content_rows, COLS, row, 0);
        wbkgd(out_win, COLOR_PAIR(CP_DEFAULT));
        scrollok(out_win, TRUE);
        leaveok(out_win, TRUE);
        row += content_rows;
    }

    /* Separator above input */
    sep2_win = newwin(1, COLS, row, 0);
    leaveok(sep2_win, TRUE);
    fill_sep(sep2_win, " Input ");
    row++;

    /* Input area — no border, just open lines */
    input_win = newwin(INPUT_OUTER, COLS, row, 0);
    wbkgd(input_win, COLOR_PAIR(CP_INPUT));
    leaveok(input_win, FALSE);
    keypad(input_win, TRUE);
    nodelay(input_win, TRUE);
    row += INPUT_OUTER;

    /* Function key hint bar */
    fkey_win = newwin(1, COLS, row, 0);
    wbkgd(fkey_win, COLOR_PAIR(CP_STATUS));
    leaveok(fkey_win, TRUE);
}

void draw_status(void)
{
    if (!status_win) return;
    werase(status_win);
    wbkgd(status_win, COLOR_PAIR(CP_STATUS));
    wattron(status_win, COLOR_PAIR(CP_STATUS));

    /* Left: program name */
    mvwprintw(status_win, 0, 1, "TermTCP %s", VERSION);

    /* Center: session indicator + connection info */
    char center[80];
    int  nsess = session_count();
    char sess_tag[16] = "";
    if (nsess > 1)
        snprintf(sess_tag, sizeof(sess_tag), "[%d/%d] ", session_index() + 1, nsess);
    if (S->connected && S->host_idx >= 0) {
        int elapsed = (int)(time(NULL) - S->connect_time);
        snprintf(center, sizeof(center), "%s%s  [%02d:%02d]",
                 sess_tag, hosts[S->host_idx].name, elapsed / 60, elapsed % 60);
    } else {
        snprintf(center, sizeof(center), "%sNot Connected", sess_tag);
    }
    int cx = (COLS - (int)strlen(center)) / 2;
    if (cx > 0) mvwprintw(status_win, 0, cx, "%s", center);

    /* Right: UTC time + log indicator */
    time_t now = time(NULL);
    struct tm *t = gmtime(&now);
    char right[32];
    snprintf(right, sizeof(right), "%s%02d:%02d UTC ",
             S->logging ? "LOG  " : "", t->tm_hour, t->tm_min);
    int rx = COLS - (int)strlen(right);
    if (rx > 0) mvwprintw(status_win, 0, rx, "%s", right);

    wattroff(status_win, COLOR_PAIR(CP_STATUS));
    wrefresh(status_win);
}

static void draw_fkeys(void)
{
    if (!fkey_win) return;
    werase(fkey_win);

    static const struct { const char *key; const char *desc; } fkeys[] = {
        { "F1",  "Help"    },
        { "F3",  "Connect" },
        { "F4",  "Disc"    },
        { "F5",  "Forms"   },
        { "F6",  "Log"     },
        { "F7",  "New Ses" },
        { "F8",  "Prev"    },
        { "F9",  "Next"    },
        { "F10", "Quit"    },
    };
    static const int nfkeys = 9;

    int col = 0;
    for (int i = 0; i < nfkeys && col < COLS - 2; i++) {
        wattron(fkey_win, COLOR_PAIR(CP_FKEY) | A_BOLD);
        mvwprintw(fkey_win, 0, col, "%s", fkeys[i].key);
        col += (int)strlen(fkeys[i].key);
        wattroff(fkey_win, COLOR_PAIR(CP_FKEY) | A_BOLD);
        wattron(fkey_win, COLOR_PAIR(CP_STATUS));
        mvwprintw(fkey_win, 0, col, " %-*s", (int)strlen(fkeys[i].desc) + 1, fkeys[i].desc);
        col += (int)strlen(fkeys[i].desc) + 2;
        wattroff(fkey_win, COLOR_PAIR(CP_STATUS));
    }
    /* Fill remainder of bar */
    wattron(fkey_win, COLOR_PAIR(CP_STATUS));
    for (int c = col; c < COLS; c++) mvwaddch(fkey_win, 0, c, ' ');
    wattroff(fkey_win, COLOR_PAIR(CP_STATUS));

    wrefresh(fkey_win);
}

/* Render stored lines from a ring buffer into an ncurses window.
 * scroll_off=0 shows the most recent lines (live); >0 scrolls back. */
static void scrl_draw_window(WINDOW *win, ScrlLine *buf, int count, int head,
                              int scroll_off)
{
    if (!win) return;
    scrollok(win, FALSE);   /* disable scroll during batch redraw — prevents losing lines */
    werase(win);
    if (count == 0) { scrollok(win, TRUE); wrefresh(win); return; }

    int win_h, win_w;
    getmaxyx(win, win_h, win_w);
    (void)win_w;

    int last  = count - 1 - scroll_off;
    if (last < 0) { wrefresh(win); return; }
    int first = last - win_h + 1;
    if (first < 0) first = 0;

    for (int i = first; i <= last; i++) {
        int slot = (head - count + i + SCRL_MAX) % SCRL_MAX;
        const ScrlLine *sl  = &buf[slot];
        const char     *p   = sl->data;
        const char     *end = p + sl->len;
        int             cur = sl->base_attr;
        wattron(win, cur);
        while (p < end) {
            unsigned char c = (unsigned char)*p;
            if (c == 0x1B && (p + 1) < end) {
                wattroff(win, cur);
                cur = bpq_color_attr((unsigned char)p[1]);
                wattron(win, cur);
                p += 2;
            } else if (c >= 32 && c < 127) { waddch(win, c); p++; }
            else { p++; }
        }
        wattroff(win, cur);
        if (i < last) waddch(win, '\n');
    }
    /* Move cursor to start of next blank row so live appends begin on a fresh line */
    int drawn = last - first + 1;
    if (drawn < win_h) wmove(win, drawn, 0);
    scrollok(win, TRUE);    /* re-enable for live appends */
    wrefresh(win);
}

void append_mon(const char *text, int len, int attr)
{
    if (!mon_visible || len <= 0) return;

    const char *p   = text;
    const char *end = text + len;
    int live = (mon_win != NULL && !suppress_draw);

    S->mon_part_cur = attr;
    if (S->mon_part_len == 0) S->mon_part_base = attr;

    if (live && S->mon_scroll == 0)
        wattron(mon_win, S->mon_part_cur);

    while (p < end) {
        unsigned char c = (unsigned char)*p;
        if (c == 0x1B && (p + 1) < end) {
            int na = bpq_color_attr((unsigned char)p[1]);
            if (S->mon_part_len + 2 < SCRL_W) {
                S->mon_part[S->mon_part_len++] = (char)0x1B;
                S->mon_part[S->mon_part_len++] = p[1];
            }
            if (live && S->mon_scroll == 0) {
                wattroff(mon_win, S->mon_part_cur);
                wattron(mon_win, na);
            }
            S->mon_part_cur = na;
            p += 2;
        } else if (c == '\r' || c == '\n') {
            ScrlLine *sl   = &S->mon_sbuf[S->mon_sbuf_head];
            memcpy(sl->data, S->mon_part, S->mon_part_len);
            sl->len        = S->mon_part_len;
            sl->base_attr  = S->mon_part_base;
            S->mon_sbuf_head  = (S->mon_sbuf_head + 1) % SCRL_MAX;
            if (S->mon_sbuf_count < SCRL_MAX) S->mon_sbuf_count++;
            S->mon_part_len   = 0;
            S->mon_part_base  = S->mon_part_cur;
            if (live && S->mon_scroll == 0)
                waddch(mon_win, '\n');
            p++;
        } else if (c >= 32 && c < 127) {
            if (S->mon_part_len < SCRL_W - 1)
                S->mon_part[S->mon_part_len++] = (char)c;
            if (live && S->mon_scroll == 0)
                waddch(mon_win, c);
            p++;
        } else { p++; }
    }

    if (live && S->mon_scroll == 0) {
        wattroff(mon_win, S->mon_part_cur);
        wrefresh(mon_win);
        if (input_win) wrefresh(input_win);
    }
}

void append_out(const char *text, int len, int attr)
{
    if (len <= 0) return;

    const char *p   = text;
    const char *end = text + len;
    int live = (out_win != NULL && !suppress_draw);

    S->out_part_cur = attr;
    if (S->out_part_len == 0) S->out_part_base = attr;

    if (live && S->out_scroll == 0)
        wattron(out_win, S->out_part_cur);

    while (p < end) {
        unsigned char c = (unsigned char)*p;
        if (c == 0x1B && (p + 1) < end) {
            int na = bpq_color_attr((unsigned char)p[1]);
            if (S->out_part_len + 2 < SCRL_W) {
                S->out_part[S->out_part_len++] = (char)0x1B;
                S->out_part[S->out_part_len++] = p[1];
            }
            if (live && S->out_scroll == 0) {
                wattroff(out_win, S->out_part_cur);
                wattron(out_win, na);
            }
            S->out_part_cur = na;
            p += 2;
        } else if (c == '\r' || c == '\n') {
            ScrlLine *sl   = &S->out_sbuf[S->out_sbuf_head];
            memcpy(sl->data, S->out_part, S->out_part_len);
            sl->len        = S->out_part_len;
            sl->base_attr  = S->out_part_base;
            S->out_sbuf_head  = (S->out_sbuf_head + 1) % SCRL_MAX;
            if (S->out_sbuf_count < SCRL_MAX) S->out_sbuf_count++;
            S->out_part_len   = 0;
            S->out_part_base  = S->out_part_cur;
            if (live && S->out_scroll == 0)
                waddch(out_win, '\n');
            p++;
        } else if (c >= 32 && c < 127) {
            if (S->out_part_len < SCRL_W - 1)
                S->out_part[S->out_part_len++] = (char)c;
            if (live && S->out_scroll == 0)
                waddch(out_win, c);
            p++;
        } else { p++; }
    }

    if (live && S->out_scroll == 0) {
        wattroff(out_win, S->out_part_cur);
        wrefresh(out_win);
        if (input_win) wrefresh(input_win);
    }
}

void draw_input(void)
{
    if (!input_win) return;

    /* Refresh separator above */
    if (sep2_win) fill_sep(sep2_win, " Input ");

    werase(input_win);
    wattron(input_win, COLOR_PAIR(CP_INPUT));

    /* Prompt on first row */
    mvwaddstr(input_win, 0, 0, "> ");

    /* Buffer content — wraps at COLS, '> ' prefix is 2 chars */
    for (int i = 0; i < S->input_len; i++) {
        int pos = 2 + i;
        int r   = pos / COLS;
        int c   = pos % COLS;
        if (r >= INPUT_OUTER) break;
        mvwaddch(input_win, r, c, (unsigned char)S->input_buf[i]);
    }

    /* Cursor position */
    int cpos = 2 + S->input_len;
    int cr   = cpos / COLS;
    int cc   = cpos % COLS;
    if (cr < INPUT_OUTER) wmove(input_win, cr, cc);

    wattroff(input_win, COLOR_PAIR(CP_INPUT));
    wrefresh(input_win);
}

/* ═══════════════════════════════════════════════════════════
 * In-program config editor  (Ctrl+A -> O)
 * ═══════════════════════════════════════════════════════════ */

static void scrl_redraw_out(void)
{
    scrl_draw_window(out_win, S->out_sbuf, S->out_sbuf_count, S->out_sbuf_head, S->out_scroll);
}

static void scrl_redraw_mon(void)
{
    if (!mon_visible) return;
    scrl_draw_window(mon_win, S->mon_sbuf, S->mon_sbuf_count, S->mon_sbuf_head, S->mon_scroll);
}

static void update_sep_labels(void)
{
    if (sep_win) {
        if (S->out_scroll > 0)
            fill_sep(sep_win, " Output [scrolled - PgDn=live] ");
        else
            fill_sep(sep_win, " Output ");
    }
}

static void refresh_all(void)
{
    draw_status();
    update_sep_labels();
    if (sep2_win) fill_sep(sep2_win, " Input ");
    scrl_redraw_out();
    scrl_redraw_mon();
    draw_input();
    draw_fkeys();
}

/* Full background repaint using redrawwin() — safe alternative to
 * clearok(curscr) which resets ACS mode and breaks box() borders. */
static void force_repaint(void)
{
    if (status_win) redrawwin(status_win);
    if (sep_win)    redrawwin(sep_win);
    if (out_win)    redrawwin(out_win);
    if (mon_win)    redrawwin(mon_win);
    if (sep2_win)   redrawwin(sep2_win);
    if (input_win)  redrawwin(input_win);
    if (fkey_win)   redrawwin(fkey_win);
    refresh_all();
}

static void config_menu(void)
{
    int mh = 18, mw = 64;
    int my = (LINES - mh) / 2;
    int mx = (COLS  - mw) / 2;
    if (my < 0) my = 0;
    if (mx < 0) mx = 0;

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    keypad(w, TRUE);
    nodelay(w, FALSE);

    const char *labels[]   = { "Host:", "Port:", "User:", "Pass:", "Name:" };
    const int   field_y[]  = { 4, 6, 8, 10, 12 };
    const int   max_len[]  = { 99, 9, 79, 79, 49 };
    char        fields[5][100];

    int cur_host  = (S->host_idx >= 0 && S->host_idx < num_hosts) ? S->host_idx : 0;
    int cur_field = 0;

    if (num_hosts == 0) { memset(&hosts[0], 0, sizeof(HostEntry)); num_hosts = 1; }

#define CF_COMMIT() do { \
    strncpy(hosts[cur_host].host,     fields[0], 99); hosts[cur_host].host[99]    =0; \
    strncpy(hosts[cur_host].port,     fields[1],  9); hosts[cur_host].port[9]     =0; \
    strncpy(hosts[cur_host].username, fields[2], 79); hosts[cur_host].username[79]=0; \
    strncpy(hosts[cur_host].password, fields[3], 79); hosts[cur_host].password[79]=0; \
    strncpy(hosts[cur_host].name,     fields[4], 49); hosts[cur_host].name[49]    =0; \
} while(0)

    int running = 1;
    while (running) {
        if (cur_host >= num_hosts) cur_host = num_hosts - 1;
        if (cur_host < 0)         cur_host = 0;

        strncpy(fields[0], hosts[cur_host].host,     99); fields[0][99]=0;
        strncpy(fields[1], hosts[cur_host].port,      9); fields[1][9] =0;
        strncpy(fields[2], hosts[cur_host].username, 79); fields[2][79]=0;
        strncpy(fields[3], hosts[cur_host].password, 79); fields[3][79]=0;
        strncpy(fields[4], hosts[cur_host].name,     49); fields[4][49]=0;

        werase(w); draw_box(w);
        mvwprintw(w, 0, (mw-14)/2, " Config Hosts ");
        mvwprintw(w, 2, 2, "Host %d of %d   PgUp/PgDn=switch  N=new  D=delete",
                  cur_host+1, num_hosts);

        for (int i = 0; i < 5; i++) {
            mvwprintw(w, field_y[i], 2, "%-6s", labels[i]);
            if (i == cur_field) wattron(w, A_REVERSE);
            mvwprintw(w, field_y[i], 9, "%-52s", fields[i]);
            if (i == cur_field) wattroff(w, A_REVERSE);
        }

        mvwprintw(w, mh-4, 2, "Tab / Up / Down = move between fields");
        mvwprintw(w, mh-3, 2, "Enter / F1 = save & close     Esc / Q = cancel");
        mvwprintw(w, mh-2, 2, "Backspace = delete char");

        int flen = (int)strlen(fields[cur_field]);
        wmove(w, field_y[cur_field], 9 + flen);
        wrefresh(w);

        int ch = wgetch(w);
        switch (ch) {
        case KEY_PPAGE:
            CF_COMMIT();
            if (cur_host > 0) { cur_host--; cur_field = 0; }
            break;
        case KEY_NPAGE:
            CF_COMMIT();
            if (cur_host < num_hosts-1) { cur_host++; cur_field = 0; }
            break;
        case 'N': case 'n':
            if (num_hosts < MAX_HOSTS) {
                CF_COMMIT();
                memset(&hosts[num_hosts], 0, sizeof(HostEntry));
                num_hosts++; cur_host = num_hosts-1; cur_field = 0;
            }
            break;
        case 'D': case 'd':
            if (num_hosts > 1) {
                for (int i = cur_host; i < num_hosts-1; i++) hosts[i] = hosts[i+1];
                num_hosts--;
                if (cur_host >= num_hosts) cur_host = num_hosts-1;
            }
            break;
        case '\t': case KEY_DOWN:
            CF_COMMIT(); cur_field = (cur_field + 1) % 5;
            break;
        case KEY_UP:
            CF_COMMIT(); cur_field = (cur_field + 4) % 5;
            break;
        case '\r': case '\n': case KEY_ENTER: case KEY_F(1):
            CF_COMMIT(); save_config(); running = 0;
            break;
        case 27: case 'Q': case 'q':
            running = 0;
            break;
        case KEY_BACKSPACE: case 127: case '\b': {
            int fl = (int)strlen(fields[cur_field]);
            if (fl > 0) { fields[cur_field][fl-1] = '\0'; CF_COMMIT(); }
            break;
        }
        default:
            if (ch >= 32 && ch < 127) {
                int fl = (int)strlen(fields[cur_field]);
                if (fl < max_len[cur_field]-1) {
                    fields[cur_field][fl]   = (char)ch;
                    fields[cur_field][fl+1] = '\0';
                    CF_COMMIT();
                }
            }
            break;
        }
    }
#undef CF_COMMIT

    delwin(w);
    force_repaint();
}

/* ═══════════════════════════════════════════════════════════
 * Multi-line textarea editor (used by the General Message form)
 *
 * Operates on an IcsMessage's msg_lines[]/msg_nlines directly.
 * Enter splits/creates a new logical line, arrows move the cursor,
 * Backspace joins with the previous line at column 0.
 * w must already be sized at least ICS_MSG_ROWS+2 x ICS_MSG_COLS+2
 * (the box border eats one row/col on each side).
 * Returns when the caller's exit key (Tab/Esc handled by caller via
 * the return value) is pressed; ch_out receives that key.
 * ═══════════════════════════════════════════════════════════ */

typedef struct {
    char (*lines)[ICS_MSG_LINELEN];
    int  *nlines;
    int   cur_line;
    int   cur_col;
    int   top_line;     /* first visible line (vertical scroll) */
} TextArea;

static void ta_clamp(TextArea *ta, int rows)
{
    if (*ta->nlines < 1) *ta->nlines = 1;
    if (ta->cur_line >= *ta->nlines) ta->cur_line = *ta->nlines - 1;
    if (ta->cur_line < 0) ta->cur_line = 0;
    int llen = (int)strlen(ta->lines[ta->cur_line]);
    if (ta->cur_col > llen) ta->cur_col = llen;
    if (ta->cur_col < 0) ta->cur_col = 0;
    if (ta->cur_line < ta->top_line) ta->top_line = ta->cur_line;
    if (ta->cur_line >= ta->top_line + rows) ta->top_line = ta->cur_line - rows + 1;
    if (ta->top_line < 0) ta->top_line = 0;
}

/* Draws the textarea inside window w at the given inner origin/size
 * (caller already drew the surrounding box/labels). */
static void ta_draw(WINDOW *w, TextArea *ta, int oy, int ox, int rows, int cols)
{
    for (int r = 0; r < rows; r++) {
        int line_idx = ta->top_line + r;
        mvwprintw(w, oy + r, ox, "%-*s", cols, "");  /* clear row */
        if (line_idx < *ta->nlines) {
            const char *s = ta->lines[line_idx];
            int slen = (int)strlen(s);
            int show = slen < cols ? slen : cols;
            mvwaddnstr(w, oy + r, ox, s, show);
        }
    }
    int crow = oy + (ta->cur_line - ta->top_line);
    int ccol = ox + ta->cur_col;
    wmove(w, crow, ccol);
}

/* Handles one keystroke. Returns 0 to keep editing, or the raw key
 * code that the caller should treat as an "exit/switch focus" key
 * (currently: '\t' and 27/Esc; caller decides what those mean).
 * cols = visible display width of the textarea; used for auto word-wrap. */
static int ta_handle_key(TextArea *ta, int ch, int rows, int cols)
{
    switch (ch) {
    case '\t':
    case 27:
        return ch;

    case KEY_UP:
        ta->cur_line--;
        break;
    case KEY_DOWN:
        ta->cur_line++;
        break;
    case KEY_LEFT:
        if (ta->cur_col > 0) ta->cur_col--;
        else if (ta->cur_line > 0) {
            ta->cur_line--;
            ta->cur_col = (int)strlen(ta->lines[ta->cur_line]);
        }
        break;
    case KEY_RIGHT: {
        int llen = (int)strlen(ta->lines[ta->cur_line]);
        if (ta->cur_col < llen) ta->cur_col++;
        else if (ta->cur_line < *ta->nlines - 1) {
            ta->cur_line++;
            ta->cur_col = 0;
        }
        break;
    }
    case KEY_HOME:
        ta->cur_col = 0;
        break;
    case KEY_END:
        ta->cur_col = (int)strlen(ta->lines[ta->cur_line]);
        break;

    case '\r': case '\n': case KEY_ENTER: {
        if (*ta->nlines >= ICS_MSG_LINES) break;  /* full */
        /* Split current line at cursor into a new line below */
        char *cur = ta->lines[ta->cur_line];
        char  tail[ICS_MSG_LINELEN];
        strncpy(tail, cur + ta->cur_col, ICS_MSG_LINELEN - 1);
        tail[ICS_MSG_LINELEN - 1] = '\0';
        cur[ta->cur_col] = '\0';
        for (int i = *ta->nlines; i > ta->cur_line + 1; i--)
            memcpy(ta->lines[i], ta->lines[i-1], ICS_MSG_LINELEN);
        strncpy(ta->lines[ta->cur_line + 1], tail, ICS_MSG_LINELEN - 1);
        ta->lines[ta->cur_line + 1][ICS_MSG_LINELEN - 1] = '\0';
        (*ta->nlines)++;
        ta->cur_line++;
        ta->cur_col = 0;
        break;
    }

    case KEY_BACKSPACE: case 127: case '\b':
        if (ta->cur_col > 0) {
            char *cur = ta->lines[ta->cur_line];
            int   len = (int)strlen(cur);
            memmove(cur + ta->cur_col - 1, cur + ta->cur_col, len - ta->cur_col + 1);
            ta->cur_col--;
        } else if (ta->cur_line > 0) {
            /* Join with previous line */
            char *prev = ta->lines[ta->cur_line - 1];
            char *cur  = ta->lines[ta->cur_line];
            int   plen = (int)strlen(prev);
            int   clen = (int)strlen(cur);
            if (plen + clen < ICS_MSG_LINELEN - 1) {
                memcpy(prev + plen, cur, clen + 1);
                for (int i = ta->cur_line; i < *ta->nlines - 1; i++)
                    memcpy(ta->lines[i], ta->lines[i+1], ICS_MSG_LINELEN);
                (*ta->nlines)--;
                ta->cur_line--;
                ta->cur_col = plen;
            }
        }
        break;

    case KEY_DC: {  /* Delete key: remove char at cursor */
        char *cur = ta->lines[ta->cur_line];
        int   len = (int)strlen(cur);
        if (ta->cur_col < len) {
            memmove(cur + ta->cur_col, cur + ta->cur_col + 1, len - ta->cur_col);
        } else if (ta->cur_line < *ta->nlines - 1) {
            char *next = ta->lines[ta->cur_line + 1];
            int   nlen = (int)strlen(next);
            if (len + nlen < ICS_MSG_LINELEN - 1) {
                memcpy(cur + len, next, nlen + 1);
                for (int i = ta->cur_line + 1; i < *ta->nlines - 1; i++)
                    memcpy(ta->lines[i], ta->lines[i+1], ICS_MSG_LINELEN);
                (*ta->nlines)--;
            }
        }
        break;
    }

    default:
        if (ch >= 32 && ch < 256) {
            char *cur = ta->lines[ta->cur_line];
            int   len = (int)strlen(cur);
            /* Auto word-wrap when the visible line width is reached */
            if (len >= cols - 1 && *ta->nlines < ICS_MSG_LINES) {
                int brk = len - 1;
                while (brk > 0 && cur[brk] != ' ') brk--;
                if (brk == 0) brk = len;   /* no space: hard break at end */
                int skip = (brk < len && cur[brk] == ' ') ? 1 : 0;
                char tail[ICS_MSG_LINELEN];
                strncpy(tail, cur + brk + skip, ICS_MSG_LINELEN - 1);
                tail[ICS_MSG_LINELEN - 1] = '\0';
                cur[brk] = '\0';
                for (int i = *ta->nlines; i > ta->cur_line + 1; i--)
                    memcpy(ta->lines[i], ta->lines[i-1], ICS_MSG_LINELEN);
                strncpy(ta->lines[ta->cur_line + 1], tail, ICS_MSG_LINELEN - 1);
                ta->lines[ta->cur_line + 1][ICS_MSG_LINELEN - 1] = '\0';
                (*ta->nlines)++;
                ta->cur_line++;
                ta->cur_col = (int)strlen(ta->lines[ta->cur_line]);
                cur = ta->lines[ta->cur_line];
            }
            len = (int)strlen(cur);
            if (len < ICS_MSG_LINELEN - 1 && ta->cur_col <= len) {
                memmove(cur + ta->cur_col + 1, cur + ta->cur_col, len - ta->cur_col + 1);
                cur[ta->cur_col] = (char)ch;
                ta->cur_col++;
            }
        }
        break;
    }

    ta_clamp(ta, rows);
    return 0;
}

/* ═══════════════════════════════════════════════════════════
 * Monitor options  (Ctrl+A -> M)
 * ═══════════════════════════════════════════════════════════ */

/* Returns 'P' to switch to port page, 0 to close */
static int mon_options_page(void)
{
    static const char *labels[] = {
        "Monitor TX",
        "Monitor Supervisor",
        "Monitor Nodes",
        "Monitor UI Only",
        "Enable Colour",
        "Show Monitor Pane",
    };
    int *vals[] = { &mon_tx, &mon_com, &mon_nodes, &mon_ui_only, &mon_colour, &mon_visible };
    const int N = 6;
    int mh = N + 5, mw = 46;
    int my = (LINES - mh) / 2; if (my < 0) my = 0;
    int mx = (COLS  - mw) / 2; if (mx < 0) mx = 0;

    force_repaint();

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    keypad(w, TRUE);
    nodelay(w, FALSE);

    int cur = 0;
    for (;;) {
        werase(w);
        draw_box(w);
        mvwprintw(w, 0, (mw - 18) / 2, " Monitor Options ");
        for (int i = 0; i < N; i++) {
            if (i == cur) wattron(w, A_REVERSE);
            mvwprintw(w, 2 + i, 3, "[%c] %s", *vals[i] ? 'X' : ' ', labels[i]);
            if (i == cur) wattroff(w, A_REVERSE);
        }
        mvwprintw(w, mh - 3, 3, "Up/Down=move  Space=toggle");
        mvwprintw(w, mh - 2, 3, "Tab=Port Select  Esc/Q=close");
        wrefresh(w);

        int ch = wgetch(w);
        switch (ch) {
        case KEY_UP:   cur = (cur - 1 + N) % N; break;
        case KEY_DOWN: cur = (cur + 1) % N;      break;
        case '\t':
            delwin(w);
            return 'P';
        case ' ': case '\r': case '\n': case KEY_ENTER:
            if (cur == 5) {
                mon_visible = !mon_visible;
                save_config();
                delwin(w);
                create_windows();
                force_repaint();
                return 0;
            }
            *vals[cur] = !(*vals[cur]);
            send_trace_options();
            save_config();
            break;
        case 27: case 'Q': case 'q':
            delwin(w);
            force_repaint();
            return 0;
        }
    }
}

/* Returns 'O' to switch to options page, 0 to close */
static int mon_ports_page(void)
{
    int port_list[32], n_ports = 0;
    for (int b = 0; b < 32; b++)
        if (S->port_seen & (1 << b)) port_list[n_ports++] = b + 1;
    if (n_ports == 0)
        for (int b = 0; b < 8; b++) port_list[n_ports++] = b + 1;

    int mh = n_ports + 5; if (mh < 10) mh = 10;
    int mw = 54;
    int my = (LINES - mh) / 2; if (my < 0) my = 0;
    int mx = (COLS  - mw) / 2; if (mx < 0) mx = 0;

    force_repaint();

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    keypad(w, TRUE);
    nodelay(w, FALSE);

    int cur = 0;
    for (;;) {
        werase(w);
        draw_box(w);
        mvwprintw(w, 0, (mw - 16) / 2, " Port Selection ");
        if (S->port_seen == 0)
            mvwprintw(w, 2, 3, "(connect to auto-detect ports)");
        for (int i = 0; i < n_ports; i++) {
            int pn = port_list[i];
            if (i == cur) wattron(w, A_REVERSE);
            mvwprintw(w, 2 + i, 3, "[%c] %2d  %-32s",
                      (port_mask >> (pn - 1)) & 1 ? 'X' : ' ',
                      pn, port_names[pn - 1]);
            if (i == cur) wattroff(w, A_REVERSE);
        }
        mvwprintw(w, mh - 3, 3, "Up/Down=move  Space=toggle");
        mvwprintw(w, mh - 2, 3, "Tab=Monitor Opts  Esc/Q=close");
        wrefresh(w);

        int ch = wgetch(w);
        switch (ch) {
        case KEY_UP:   cur = (cur - 1 + n_ports) % n_ports; break;
        case KEY_DOWN: cur = (cur + 1) % n_ports;            break;
        case '\t':
            delwin(w);
            return 'O';
        case ' ': case '\r': case '\n': case KEY_ENTER:
            port_mask ^= (1 << (port_list[cur] - 1));
            send_trace_options();
            save_config();
            break;
        case 27: case 'Q': case 'q':
            delwin(w);
            force_repaint();
            return 0;
        }
    }
}

static void monitor_options_menu(void)
{
    int next = 'O';
    while (next) {
        if (next == 'O') next = mon_options_page();
        else             next = mon_ports_page();
    }
}

/* ═══════════════════════════════════════════════════════════
 * Help overlay
 * ═══════════════════════════════════════════════════════════ */

static void show_help(void)
{
    int mh = 19, mw = 52;
    WINDOW *w = newwin(mh, mw, (LINES-mh)/2, (COLS-mw)/2);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    draw_box(w);
    mvwprintw(w, 0, (mw-8)/2, " Help ");

    mvwprintw(w,  2, 3, "Ctrl+A           Open menu");
    mvwprintw(w,  3, 3, "Ctrl+A  C        Connect");
    mvwprintw(w,  4, 3, "Ctrl+A  D        Disconnect");
    mvwprintw(w,  5, 3, "Ctrl+A  O        Config / host editor");
    mvwprintw(w,  6, 3, "Ctrl+A  M        Monitor options");
    mvwprintw(w,  7, 3, "Ctrl+A  L        Toggle log");
    mvwprintw(w,  8, 3, "Ctrl+A  F        Forms & Check-Ins");
    mvwprintw(w,  9, 3, "Ctrl+A  H        This help");
    mvwprintw(w, 10, 3, "Ctrl+A  Q        Quit");
    mvwprintw(w, 12, 3, "Enter            Send line to server");
    mvwprintw(w, 13, 3, "Up / Down        Input history");
    mvwprintw(w, 14, 3, "PgUp / PgDn      Scroll output window");
    mvwprintw(w, 15, 3, "Home / End       Scroll monitor window");
    mvwprintw(w, 16, 3, "Backspace        Delete char");
    mvwprintw(w, mh-2, 3, "Press any key to close");

    wrefresh(w);
    nodelay(w, FALSE);
    wgetch(w);
    delwin(w);
    force_repaint();
}

/* ═══════════════════════════════════════════════════════════
 * ICS-213 General Message  (Ctrl+A -> G)
 *
 * F2 always sends over a dedicated background connection (see
 * "Background send connection" below) rather than the main
 * session's socket, so a send never disturbs whatever is happening
 * in the live monitor/output windows.
 * ═══════════════════════════════════════════════════════════ */

#define BG_PROMPT_TIMEOUT_MS  10000  /* max ms to wait for BBS to stop talking */
#define BG_QUIET_MS             300  /* silence this long after data = BBS at prompt */

/* ── Background send connection ──────────────────────────────
 * F2 always opens its OWN socket for sending, completely separate
 * from the main interactive session's global `S->sock`/`S->connected`.
 * This means a send never disturbs whatever you're doing in the
 * main window - no shared connection state, no risk of interleaving
 * a send with whatever you're typing or watching scroll by.
 *
 * Deliberately does NOT call bpq_process(): that function's monitor
 * frame parser keeps state in globals (S->in_mon_frame, S->mon_flen,
 * S->ports_parse_mode, etc.) shared with the main session. Running two
 * sockets' data through one shared parser would corrupt both. The
 * background connection instead does a much simpler raw drain: BPQ
 * monitor frames are bounded by 0xFF/0xFE bytes, so they're just
 * stripped out wholesale rather than decoded, since the background
 * send has no monitor window of its own to display them in anyway.
 *
 * All progress/results are echoed into the same out_win as the main
 * session, prefixed "[BG]" so it's visually distinct from anything
 * happening on the live connection.
 * ──────────────────────────────────────────────────────────── */

/* Strips BPQ monitor-frame markers (0xFF ... 0xFE) from raw bytes
 * before treating the rest as plain session text, since the
 * background connection has no monitor window to decode them into.
 * Writes the surviving plain-text bytes into out (caller-sized) and
 * returns how many bytes were written. */
static int bg_strip_monitor_frames(const char *buf, int len, char *out, int outcap)
{
    int  outlen = 0;
    int  in_frame = 0;
    for (int i = 0; i < len && outlen < outcap; i++) {
        unsigned char c = (unsigned char)buf[i];
        if (!in_frame) {
            if (c == 0xFF) { in_frame = 1; continue; }
            out[outlen++] = (char)c;
        } else {
            if (c == 0xFE) in_frame = 0;
            /* else: still inside a monitor frame, discard the byte */
        }
    }
    return outlen;
}

/* Wait for the BBS to finish talking and reach a prompt.
 * Strategy: keep reading until BG_QUIET_MS of silence after the last
 * received byte — that silence means the BBS is waiting for input.
 * Hard timeout at BG_PROMPT_TIMEOUT_MS in case the server never responds.
 * Returns 1 if a BBS error keyword was seen, 0 otherwise. */
static int bg_wait_for_prompt(int fd)
{
    char rbuf[2048], stripped[2048], lc[2048];
    struct timeval start, now, last_rx;
    int got_data = 0;
    gettimeofday(&start, NULL);

    for (;;) {
        gettimeofday(&now, NULL);
        long total = (now.tv_sec  - start.tv_sec)  * 1000L
                   + (now.tv_usec - start.tv_usec) / 1000L;
        if (total >= BG_PROMPT_TIMEOUT_MS) return 0;

        /* Once data has arrived, return as soon as it's been quiet long enough */
        if (got_data) {
            long quiet = (now.tv_sec  - last_rx.tv_sec)  * 1000L
                       + (now.tv_usec - last_rx.tv_usec) / 1000L;
            if (quiet >= BG_QUIET_MS) return 0;
        }

        fd_set rfds;
        FD_ZERO(&rfds); FD_SET(fd, &rfds);
        struct timeval tv = { 0, 100000L };  /* poll every 100ms */
        int r = select(fd + 1, &rfds, NULL, NULL, &tv);
        if (r > 0 && FD_ISSET(fd, &rfds)) {
            int n = recv(fd, rbuf, sizeof(rbuf) - 1, 0);
            if (n <= 0) return 0;
            gettimeofday(&last_rx, NULL);
            got_data = 1;
            int slen = bg_strip_monitor_frames(rbuf, n, stripped, sizeof(stripped) - 1);
            if (slen > 0) {
                stripped[slen] = '\0';
                char echo[2200];
                int  elen = snprintf(echo, sizeof(echo), "[BG] %s", stripped);
                if (elen > 0) append_out(echo, elen, COLOR_PAIR(CP_MAGENTA));
                /* Lowercase copy for keyword scan so we don't clobber the echo */
                memcpy(lc, stripped, (size_t)(slen + 1));
                for (int i = 0; i < slen; i++)
                    if (lc[i] >= 'A' && lc[i] <= 'Z') lc[i] = (char)(lc[i] - 'A' + 'a');
                if (strstr(lc, "invalid") || strstr(lc, "unknown command") ||
                    strstr(lc, "not allowed") || strstr(lc, "rejected")) {
                    return 1;
                }
            }
        }
    }
}

static void bg_send_line(int fd, const char *text, int *aborted)
{
    if (*aborted || fd < 0) return;

    int len = (int)strlen(text);
    send(fd, text, len, 0);
    send(fd, "\r", 1, 0);

    char echo[ICS_MSG_LINELEN + 16];
    int  elen = snprintf(echo, sizeof(echo), "[BG] > %s\n", text);
    append_out(echo, elen, COLOR_PAIR(CP_MAGENTA) | A_BOLD);

    /* Wait for BBS to go quiet (prompt ready) before caller sends next line */
    if (bg_wait_for_prompt(fd)) *aborted = 1;
}

static void bg_send_block(int fd, char lines[][ICS_MSG_LINELEN], int n, int *aborted)
{
    if (*aborted || fd < 0 || n <= 0) return;

    for (int i = 0; i < n; i++) {
        int len = (int)strlen(lines[i]);
        send(fd, lines[i], len, 0);
        send(fd, "\r", 1, 0);

        char echo[ICS_MSG_LINELEN + 16];
        int  elen = snprintf(echo, sizeof(echo), "[BG] > %s\n", lines[i]);
        append_out(echo, elen, COLOR_PAIR(CP_MAGENTA) | A_BOLD);
    }

    if (bg_wait_for_prompt(fd)) *aborted = 1;
}

/* Opens a new socket to hosts[idx], logs in if credentials are saved,
 * and drains the login banner briefly. Returns the S->connected fd, or
 * -1 on failure (with a message already echoed to out_win). This is
 * intentionally separate from do_connect()/S->sock/S->connected - it never
 * touches the main session's connection state. */
static int bg_connect_and_login(int idx)
{
    if (idx < 0 || idx >= num_hosts) return -1;

    char msg[160];
    snprintf(msg, sizeof(msg), "[BG] Connecting to %s:%s...\n",
             hosts[idx].host, hosts[idx].port);
    append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_MAGENTA));

    int fd = tcp_connect(hosts[idx].host, hosts[idx].port);
    if (fd < 0) {
        append_out("[BG] Connection failed.\n", 24, COLOR_PAIR(CP_RED));
        return -1;
    }

    if (hosts[idx].username[0]) {
        char login[256];
        int  llen = snprintf(login, sizeof(login), "%s\r%s\rBPQTermTCP\r",
                             hosts[idx].username, hosts[idx].password);
        send(fd, login, llen, 0);
    }

    /* Wait for login banner to finish — node is at a prompt when it goes quiet */
    bg_wait_for_prompt(fd);

    /* Enter BBS — BPQ drops you at the node prompt after login */
    send(fd, "bbs\r", 4, 0);
    append_out("[BG] > bbs\n", 11, COLOR_PAIR(CP_MAGENTA) | A_BOLD);

    /* Wait for BBS greeting to finish before sending the message */
    bg_wait_for_prompt(fd);

    return fd;
}

/* ═══════════════════════════════════════════════════════════
 * Host picker (shared by Connect and the background send flow)
 * Returns the chosen host index, or -1 if cancelled / no hosts.
 * Picks automatically with no popup if exactly one host exists.
 * ═══════════════════════════════════════════════════════════ */
static int pick_host(const char *title)
{
    if (num_hosts <= 0) return -1;
    if (num_hosts == 1) return 0;

    int pw = 20;
    for (int i = 0; i < num_hosts; i++) {
        int len = snprintf(NULL, 0, "%d - %s (%s:%s)", i,
                           hosts[i].name, hosts[i].host, hosts[i].port) + 6;
        if (len > pw) pw = len;
    }
    int hint_w = snprintf(NULL, 0, "0-%d = pick   Esc = cancel", num_hosts-1) + 6;
    if (hint_w > pw) pw = hint_w;
    if (pw > COLS) pw = COLS;
    int ph = num_hosts + 5;

    WINDOW *hw = newwin(ph, pw, (LINES-ph)/2, (COLS-pw)/2);
    wbkgd(hw, COLOR_PAIR(CP_DEFAULT));
    draw_box(hw);
    int tlen = (int)strlen(title) + 2;
    mvwprintw(hw, 0, (pw - tlen) / 2 > 0 ? (pw - tlen) / 2 : 0, " %s ", title);
    for (int i = 0; i < num_hosts; i++)
        mvwprintw(hw, 2+i, 3, "%d - %s (%s:%s)", i,
                  hosts[i].name, hosts[i].host, hosts[i].port);
    mvwprintw(hw, ph-2, 3, "0-%d = pick   Esc = cancel", num_hosts-1);
    wrefresh(hw);
    nodelay(hw, FALSE);
    keypad(hw, TRUE);
    int hch = wgetch(hw);
    delwin(hw);
    force_repaint();

    if (hch >= '0' && hch < '0' + num_hosts) return hch - '0';
    return -1;
}

/* Word-wraps src into dst_lines (each up to width chars), up to max_lines.
 * Returns the number of wrapped lines produced. A blank src line produces
 * one blank output line, preserving paragraph breaks the user typed. */
static int ics_wrap_line(const char *src, char dst_lines[][ICS_MSG_LINELEN],
                          int max_lines, int width)
{
    int n = 0;
    int slen = (int)strlen(src);
    if (slen == 0) {
        if (n < max_lines) dst_lines[n++][0] = '\0';
        return n;
    }
    int pos = 0;
    while (pos < slen && n < max_lines) {
        int remaining = slen - pos;
        int take = remaining < width ? remaining : width;
        if (take < remaining) {
            /* try to break on the last space within the window */
            int brk = take;
            while (brk > 0 && src[pos + brk] != ' ') brk--;
            if (brk > 0) take = brk;
        }
        memcpy(dst_lines[n], src + pos, take);
        dst_lines[n][take] = '\0';
        n++;
        pos += take;
        while (pos < slen && src[pos] == ' ') pos++;  /* skip the break space */
    }
    return n;
}

/* Builds the full SB exchange and sends it over its OWN background
 * connection (see "Background send connection" above) - the main
 * session's connection, if any, is never touched. Echoes progress/
 * result into out_win, prefixed "[BG]". Returns 1 on apparent
 * success, 0 if cancelled/failed (no host picked, connect failed,
 * or a BBS error keyword was detected). */
static int ics_send_message(const IcsMessage *msg)
{
    int idx = pick_host("Send via host");
    if (idx < 0) {
        append_out("Send cancelled - no host selected.\n", 37,
                   COLOR_PAIR(CP_YELLOW));
        return 0;
    }

    int fd = bg_connect_and_login(idx);
    if (fd < 0) return 0;

    append_out("[BG] --- Sending ICS-213 General Message ---\n", 47,
               COLOR_PAIR(CP_MAGENTA) | A_BOLD);

    int aborted = 0;

    /* Step 1: SB line (addressee/distribution) */
    bg_send_line(fd, msg->sb_line, &aborted);

    /* Step 2: Subject, in reply to the BBS's subject prompt */
    if (!aborted) bg_send_line(fd, msg->subject, &aborted);

    /* Step 3: Body — ICS-213 numbered format, assembled then sent as one block. */
    if (!aborted) {
        /* Build raw (pre-wrap) lines */
        char raw[100][ICS_MSG_LINELEN];
        int  rn = 0;

#define RAW(fmt, ...) \
        if (rn < 100) snprintf(raw[rn++], ICS_MSG_LINELEN, fmt, ##__VA_ARGS__)

        RAW("GENERAL MESSAGE (ICS 213)");
        RAW("");
        RAW("1. Incident Name (Optional): %s", msg->incident);
        RAW("");
        RAW("2. To (Name and Position): %s", msg->to);
        RAW("");
        RAW("3. From (Name and Position): %s", msg->from);
        RAW("");
        RAW("4. Subject: %s", msg->subject);
        RAW("");
        RAW("5. Date: %s", msg->date);
        RAW("");
        RAW("6. Time: %sZ", msg->time_z);
        RAW("");
        /* Field 7: first message line shares the "7. Message:" prefix */
        if (rn < 100) {
            const char *first = (msg->msg_nlines > 0) ? msg->msg_lines[0] : "";
            snprintf(raw[rn++], ICS_MSG_LINELEN, "7. Message: %s", first);
        }
        for (int i = 1; i < msg->msg_nlines && rn < 100; i++) {
            strncpy(raw[rn], msg->msg_lines[i], ICS_MSG_LINELEN - 1);
            raw[rn++][ICS_MSG_LINELEN - 1] = '\0';
        }
        RAW("");
        RAW("8. Approved by: Name: %s Signature: %s Position/Title: %s",
            msg->appr_name, msg->appr_sig, msg->appr_pos);
        RAW("");
        RAW("9. Reply:");
        RAW("");
        RAW("10. Replied by: Name:  Signature:  Position/Title:");

#undef RAW

        /* Word-wrap each raw line and collect into the send buffer */
        char body[ICS_MSG_LINES + 60][ICS_MSG_LINELEN];
        int  bn = 0;
        for (int i = 0; i < rn; i++) {
            char wrapped[ICS_MSG_LINES][ICS_MSG_LINELEN];
            int  nw = ics_wrap_line(raw[i], wrapped, ICS_MSG_LINES, ICS_MSG_COLS);
            for (int j = 0; j < nw && bn < (int)(sizeof(body)/sizeof(body[0])); j++) {
                strncpy(body[bn], wrapped[j], ICS_MSG_LINELEN - 1);
                body[bn][ICS_MSG_LINELEN - 1] = '\0';
                bn++;
            }
        }

        bg_send_block(fd, body, bn, &aborted);
    }

    /* Step 4: terminator */
    if (!aborted) bg_send_line(fd, "/EX", &aborted);

    close(fd);

    if (aborted) {
        append_out("[BG] --- BBS reported an error - message send aborted ---\n", 60,
                   COLOR_PAIR(CP_RED) | A_BOLD);
        return 0;
    }

    append_out("[BG] --- General Message sent ---\n", 35,
               COLOR_PAIR(CP_GREEN) | A_BOLD);
    return 1;
}

/* ── Form UI ──────────────────────────────────────────────── */

/* Single-line field editor shared by several form rows. Returns the
 * key that ended editing (Tab/Up/Down/Esc/Enter) so the caller can
 * move focus. Operates in-place on buf (NUL-terminated, max maxlen-1). */
static int ics_edit_field(WINDOW *w, int y, int x, int width,
                           char *buf, int maxlen)
{
    int len = (int)strlen(buf);
    int col = len;

    for (;;) {
        mvwprintw(w, y, x, "%-*s", width, "");
        mvwaddnstr(w, y, x, buf, (int)strlen(buf));
        wmove(w, y, x + col);
        wrefresh(w);

        int ch = wgetch(w);
        switch (ch) {
        case '\t': case KEY_DOWN: case KEY_UP: case 27:
        case '\r': case '\n': case KEY_ENTER: case KEY_F(2):
            return ch;
        case KEY_LEFT:
            if (col > 0) col--;
            break;
        case KEY_RIGHT:
            if (col < len) col++;
            break;
        case KEY_HOME:
            col = 0;
            break;
        case KEY_END:
            col = len;
            break;
        case KEY_BACKSPACE: case 127: case '\b':
            if (col > 0) {
                memmove(buf + col - 1, buf + col, len - col + 1);
                col--; len--;
            }
            break;
        case KEY_DC:
            if (col < len) {
                memmove(buf + col, buf + col + 1, len - col);
                len--;
            }
            break;
        default:
            if (ch >= 32 && ch < 256 && len < maxlen - 1) {
                memmove(buf + col + 1, buf + col, len - col + 1);
                buf[col] = (char)ch;
                col++; len++;
            }
            break;
        }
    }
}

static void ics_default_datetime(char *out, int outlen)
{
    time_t     now = time(NULL);
    struct tm *t   = localtime(&now);
    snprintf(out, outlen, "%02d/%02d/%04d %02d:%02d",
             t->tm_mon + 1, t->tm_mday, t->tm_year + 1900,
             t->tm_hour, t->tm_min);
}

static void ics_default_date(char *out, int outlen)
{
    time_t     now = time(NULL);
    struct tm *t   = gmtime(&now);
    snprintf(out, outlen, "%04d-%02d-%02d",
             t->tm_year + 1900, t->tm_mon + 1, t->tm_mday);
}

static void ics_default_time(char *out, int outlen)
{
    time_t     now = time(NULL);
    struct tm *t   = gmtime(&now);
    snprintf(out, outlen, "%02d:%02d", t->tm_hour, t->tm_min);
}

enum {
    GM_SB = 0, GM_INCIDENT, GM_TO, GM_FROM, GM_SUBJECT,
    GM_DATE, GM_TIME, GM_MSG,
    GM_APPR_NAME, GM_APPR_SIG, GM_APPR_POS,
    GM_COUNT
};

static void general_message_form(void)
{
    static IcsMessage msg;
    static int initialized = 0;

    if (!initialized) {
        memset(&msg, 0, sizeof(msg));
        strncpy(msg.sb_line, "SB PKTNET@USA", ICS_SBLINE_MAX - 1);
        ics_default_date(msg.date,   ICS_FIELD_MAX);
        ics_default_time(msg.time_z, ICS_FIELD_MAX);
        msg.msg_nlines = 1;
        initialized = 1;
    } else {
        ics_default_date(msg.date,   ICS_FIELD_MAX);
        ics_default_time(msg.time_z, ICS_FIELD_MAX);
    }

    int mh = LINES - 2;
    if (mh > 30) mh = 30;
    if (mh < 20) mh = 20;
    int mw = COLS - 4;
    if (mw > ICS_MSG_COLS + 6) mw = ICS_MSG_COLS + 6;
    if (mw < 60) mw = 60;
    int my = (LINES - mh) / 2; if (my < 0) my = 0;
    int mx = (COLS  - mw) / 2; if (mx < 0) mx = 0;

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    keypad(w, TRUE);
    nodelay(w, FALSE);
    curs_set(1);

    /* Row layout:
     *  0  border+title
     *  1  blank
     *  2  SB Line
     *  3  1. Incident
     *  4  2. To
     *  5  3. From
     *  6  4. Subject   (= BBS title + ICS field 4)
     *  7  5. Date
     *  8  6. Time
     *  9  -------- 7. Message --------   (top border of textarea)
     * 10..9+msg_rows  textarea
     * 10+msg_rows     ---------------   (bottom border)
     * 11+msg_rows     8. Name
     * 12+msg_rows        Sig
     * 13+msg_rows        Position
     * 14+msg_rows     hint
     * 15+msg_rows     border   (= mh-1)
     */
    const int label_x = 2;
    const int field_x = 16;
    const int field_w = mw - field_x - 3;

    const int y_sb       = 2;
    const int y_incident = 3;
    const int y_to       = 4;
    const int y_from     = 5;
    const int y_subject  = 6;
    const int y_date     = 7;
    const int y_time     = 8;
    const int y_msgtop   = 9;   /* top '-' border doubles as "7. Message" label */

    int msg_rows = mh - 16;
    if (msg_rows > ICS_MSG_ROWS) msg_rows = ICS_MSG_ROWS;
    if (msg_rows < 4) msg_rows = 4;

    int y_msgbot   = y_msgtop + msg_rows + 1;
    int y_apprname = y_msgbot + 1;
    int y_apprsig  = y_msgbot + 2;
    int y_apprpos  = y_msgbot + 3;
    int y_hint     = y_msgbot + 4;

    int ta_cols = (ICS_MSG_COLS < field_w) ? ICS_MSG_COLS : field_w;

    TextArea ta;
    ta.lines    = msg.msg_lines;
    ta.nlines   = &msg.msg_nlines;
    ta.cur_line = 0;
    ta.cur_col  = 0;
    ta.top_line = 0;

    int focus = GM_SB;
    int running = 1;
    int send_failed = 0;

    while (running) {
        werase(w);
        draw_box(w);
        mvwprintw(w, 0, (mw - 27) / 2, " General Message (ICS-213) ");

        /* Labels */
        mvwprintw(w, y_sb,       label_x, "SB Line:");
        mvwprintw(w, y_incident, label_x, "1. Incident:");
        mvwprintw(w, y_to,       label_x, "2. To:");
        mvwprintw(w, y_from,     label_x, "3. From:");
        mvwprintw(w, y_subject,  label_x, "4. Subject:");
        mvwprintw(w, y_date,     label_x, "5. Date:");
        mvwprintw(w, y_time,     label_x, "6. Time:");
        mvwprintw(w, y_apprname, label_x, "8. Name:");
        mvwprintw(w, y_apprsig,  label_x, "   Sig:");
        mvwprintw(w, y_apprpos,  label_x, "   Position:");

        /* Field values */
        mvwprintw(w, y_sb,       field_x, "%-*s", field_w, msg.sb_line);
        mvwprintw(w, y_subject,  field_x, "%-*s", field_w, msg.subject);
        mvwprintw(w, y_incident, field_x, "%-*s", field_w, msg.incident);
        mvwprintw(w, y_to,       field_x, "%-*s", field_w, msg.to);
        mvwprintw(w, y_from,     field_x, "%-*s", field_w, msg.from);
        mvwprintw(w, y_date,     field_x, "%-*s", field_w, msg.date);
        mvwprintw(w, y_time,     field_x, "%-*s", field_w, msg.time_z);
        /* Show static "Z" suffix after the time value */
        mvwprintw(w, y_time, field_x + (int)strlen(msg.time_z), "Z");
        mvwprintw(w, y_apprname, field_x, "%-*s", field_w, msg.appr_name);
        mvwprintw(w, y_apprsig,  field_x, "%-*s", field_w, msg.appr_sig);
        mvwprintw(w, y_apprpos,  field_x, "%-*s", field_w, msg.appr_pos);

        /* Message section borders with "7. Message" label centred in the top bar */
        for (int c = label_x; c < mw - label_x && c < mw - 1; c++) {
            mvwaddch(w, y_msgtop,  c, '-');
            mvwaddch(w, y_msgbot,  c, '-');
        }
        mvwprintw(w, y_msgtop, (mw - 13) / 2, " 7. Message ");

        ta_draw(w, &ta, y_msgtop + 1, label_x + 1, msg_rows, ta_cols);

        if (send_failed)
            mvwprintw(w, y_hint, label_x, "%-*s", mw - label_x - 2,
                      "Send failed - see output. F2=retry  Esc=Close");
        else
            mvwprintw(w, y_hint, label_x, "%-*s", mw - label_x - 2,
                      "Tab/Up/Down=move  F2=Send  Esc=Close  (Time=UTC, Z auto)");

        int ch;
        switch (focus) {
        case GM_SB:        ch = ics_edit_field(w, y_sb,       field_x, field_w, msg.sb_line,   ICS_SBLINE_MAX); break;
        case GM_SUBJECT:   ch = ics_edit_field(w, y_subject,  field_x, field_w, msg.subject,   ICS_FIELD_MAX);  break;
        case GM_INCIDENT:  ch = ics_edit_field(w, y_incident, field_x, field_w, msg.incident,  ICS_FIELD_MAX);  break;
        case GM_TO:        ch = ics_edit_field(w, y_to,       field_x, field_w, msg.to,        ICS_FIELD_MAX);  break;
        case GM_FROM:      ch = ics_edit_field(w, y_from,     field_x, field_w, msg.from,      ICS_FIELD_MAX);  break;
        case GM_DATE:      ch = ics_edit_field(w, y_date,     field_x, field_w, msg.date,      ICS_FIELD_MAX);  break;
        case GM_TIME:      ch = ics_edit_field(w, y_time,     field_x, field_w, msg.time_z,    ICS_FIELD_MAX);  break;
        case GM_APPR_NAME: ch = ics_edit_field(w, y_apprname, field_x, field_w, msg.appr_name, ICS_FIELD_MAX);  break;
        case GM_APPR_SIG:  ch = ics_edit_field(w, y_apprsig,  field_x, field_w, msg.appr_sig,  ICS_FIELD_MAX);  break;
        case GM_APPR_POS:  ch = ics_edit_field(w, y_apprpos,  field_x, field_w, msg.appr_pos,  ICS_FIELD_MAX);  break;
        case GM_MSG:
        default: {
            wmove(w, y_msgtop + 1 + (ta.cur_line - ta.top_line), label_x + 1 + ta.cur_col);
            wrefresh(w);
            int raw = wgetch(w);
            if (raw == KEY_F(2)) { ch = KEY_F(2); break; }
            int r = ta_handle_key(&ta, raw, msg_rows, ta_cols);
            if (r == '\t' || r == 27) ch = r;
            else continue;
            break;
        }
        }

        send_failed = 0;

        if (ch == KEY_F(2)) {
            if (ics_send_message(&msg)) { running = 0; break; }
            redrawwin(w); wrefresh(w);
            send_failed = 1;
            continue;
        }

        if (ch == 27) { running = 0; break; }

        if (ch == '\t' || ch == KEY_DOWN || ch == '\r' || ch == '\n' || ch == KEY_ENTER) {
            focus++;
            if (focus >= GM_COUNT) focus = GM_SB;
        } else if (ch == KEY_UP) {
            focus--;
            if (focus < GM_SB) focus = GM_APPR_POS;
        }
    }

    delwin(w);
    curs_set(1);
    force_repaint();
}

/* ═══════════════════════════════════════════════════════════
 * PKTNET Packet Check-In  (Ctrl+A -> P)
 * Mirrors https://vden.org/pktnet/check_in.html
 * ═══════════════════════════════════════════════════════════ */

/* Small modal picker: shows a bordered list of options, highlights
 * *cur_idx, lets Up/Down move, Enter/Space confirms, Esc cancels
 * (leaving *cur_idx unchanged). Mirrors the existing mon_options_page
 * style of list UI already used elsewhere in this program. */
static void chkin_pick(const char *title, const char **opts, int n, int *cur_idx)
{
    int mh = n + 4; if (mh < 6) mh = 6;
    int mw = 30;
    for (int i = 0; i < n; i++) {
        int l = (int)strlen(opts[i]) + 6;
        if (l > mw) mw = l;
    }
    int my = (LINES - mh) / 2; if (my < 0) my = 0;
    int mx = (COLS  - mw) / 2; if (mx < 0) mx = 0;

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    keypad(w, TRUE);
    nodelay(w, FALSE);

    int sel = (*cur_idx >= 0 && *cur_idx < n) ? *cur_idx : 0;

    for (;;) {
        werase(w);
        draw_box(w);
        int tlen = (int)strlen(title) + 2;
        mvwprintw(w, 0, (mw - tlen) / 2 > 0 ? (mw - tlen) / 2 : 0, " %s ", title);
        for (int i = 0; i < n; i++) {
            if (i == sel) wattron(w, A_REVERSE);
            mvwprintw(w, 2 + i, 3, "%-*s", mw - 5, opts[i]);
            if (i == sel) wattroff(w, A_REVERSE);
        }
        mvwprintw(w, mh - 1, 2, "Enter=pick  Esc=cancel");
        wrefresh(w);

        int ch = wgetch(w);
        switch (ch) {
        case KEY_UP:   sel = (sel - 1 + n) % n; break;
        case KEY_DOWN: sel = (sel + 1) % n;      break;
        case ' ': case '\r': case '\n': case KEY_ENTER:
            *cur_idx = sel;
            delwin(w);
            force_repaint();
            return;
        case 27: case 'Q': case 'q':
            delwin(w);
            force_repaint();
            return;
        }
    }
}

/* Sends the check-in over its OWN background connection (same as
 * ics_send_message) - the main session's connection is never touched. */
static int chkin_send(const PktnetCheckin *f)
{
    int idx = pick_host("Send via host");
    if (idx < 0) {
        append_out("Send cancelled - no host selected.\n", 37,
                   COLOR_PAIR(CP_YELLOW));
        return 0;
    }

    int fd = bg_connect_and_login(idx);
    if (fd < 0) return 0;

    append_out("[BG] --- Sending PKTNET Packet Check-In ---\n", 46,
               COLOR_PAIR(CP_MAGENTA) | A_BOLD);

    int aborted = 0;

    bg_send_line(fd, f->sb_line, &aborted);
    if (!aborted) bg_send_line(fd, f->subject, &aborted);

    if (!aborted) {
        char raw[40][ICS_MSG_LINELEN];
        int  n = 0;

        strncpy(raw[n], "PACKET CHECK-IN", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
        if (f->agency[0]) {
            strncpy(raw[n], f->agency, ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
        }
        raw[n++][0] = '\0';

        strncpy(raw[n], "1. STATION", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
        raw[n++][0] = '\0';
        snprintf(raw[n++], ICS_MSG_LINELEN, "a. Date/Time: %s", f->date_time);
        raw[n++][0] = '\0';
        snprintf(raw[n++], ICS_MSG_LINELEN, "b. To: %s", f->to);
        raw[n++][0] = '\0';
        snprintf(raw[n++], ICS_MSG_LINELEN, "c. From: %s", f->from);
        snprintf(raw[n++], ICS_MSG_LINELEN, "d. Station Contact Name: %s", f->contact_name);
        snprintf(raw[n++], ICS_MSG_LINELEN, "e. Initial Operator(s): %s", f->operators);
        raw[n++][0] = '\0';

        strncpy(raw[n], "2. SESSION", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
        raw[n++][0] = '\0';
        snprintf(raw[n++], ICS_MSG_LINELEN, "a. Type: %s", chkin_type_opts[f->type_idx]);
        snprintf(raw[n++], ICS_MSG_LINELEN, "b. Service: AMATEUR");
        snprintf(raw[n++], ICS_MSG_LINELEN, "c. Band: %s", chkin_band_opts[f->band_idx]);
        raw[n++][0] = '\0';
        snprintf(raw[n++], ICS_MSG_LINELEN, "d. Session: %s", chkin_session_opts[f->session_idx]);
        raw[n++][0] = '\0';
        raw[n++][0] = '\0';

        strncpy(raw[n], "3. LOCATION", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
        raw[n++][0] = '\0';
        snprintf(raw[n++], ICS_MSG_LINELEN, "a. Location: %s", f->location);
        raw[n++][0] = '\0';
        snprintf(raw[n++], ICS_MSG_LINELEN, "b. GRID SQUARE: %s", f->grid);
        raw[n++][0] = '\0';
        raw[n++][0] = '\0';

        /* "4. COMMENTS:" label, then the comment text on its own line(s) so a
         * full-length first comment line can never be truncated by the label. */
        strncpy(raw[n], "4. COMMENTS:", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
        for (int i = 0; i < f->comment_nlines && n < 40; i++) {
            strncpy(raw[n], f->comment_lines[i], ICS_MSG_LINELEN - 1);
            raw[n][ICS_MSG_LINELEN-1] = 0;
            n++;
        }

        /* Word-wrap everything into one flat block, then send it all at
         * once with no per-line pacing. */
        char body[ICS_MSG_LINES][ICS_MSG_LINELEN];
        int  bn = 0;
        for (int i = 0; i < n && bn < ICS_MSG_LINES; i++) {
            char wrapped[ICS_MSG_LINES][ICS_MSG_LINELEN];
            int  nwrapped = ics_wrap_line(raw[i], wrapped, ICS_MSG_LINES, ICS_MSG_COLS);
            for (int j = 0; j < nwrapped && bn < ICS_MSG_LINES; j++) {
                strncpy(body[bn], wrapped[j], ICS_MSG_LINELEN - 1);
                body[bn][ICS_MSG_LINELEN - 1] = '\0';
                bn++;
            }
        }

        bg_send_block(fd, body, bn, &aborted);
    }

    if (!aborted) bg_send_line(fd, "/EX", &aborted);

    close(fd);

    if (aborted) {
        append_out("[BG] --- BBS reported an error - check-in send aborted ---\n", 61,
                   COLOR_PAIR(CP_RED) | A_BOLD);
        return 0;
    }

    append_out("[BG] --- Packet Check-In sent ---\n", 35,
               COLOR_PAIR(CP_GREEN) | A_BOLD);
    return 1;
}

enum {
    CK_SB = 0, CK_SUBJECT, CK_AGENCY, CK_DATETIME, CK_TO, CK_FROM,
    CK_CONTACT, CK_OPERATORS, CK_TYPE, CK_BAND, CK_SESSION,
    CK_LOCATION, CK_GRID, CK_COMMENTS, CK_COUNT
};

static void pktnet_checkin_form(void)
{
    static PktnetCheckin f;
    static int initialized = 0;

    if (!initialized) {
        memset(&f, 0, sizeof(f));
        strncpy(f.sb_line, "SB PKTNET@USA", ICS_SBLINE_MAX - 1);
        strncpy(f.to,      "PKTNET@USA",    CHKIN_FIELD_MAX - 1);
        if (S->host_idx >= 0 && hosts[S->host_idx].username[0])
            strncpy(f.from, hosts[S->host_idx].username, CHKIN_FIELD_MAX - 1);
        f.type_idx    = 0;  /* EXERCISE */
        f.band_idx    = 2;  /* VHF      */
        f.session_idx = 1;  /* AX25 Packet */
        f.comment_nlines = 1;
        initialized = 1;
    }
    ics_default_datetime(f.date_time, ICS_DT_MAX);

    int mh = LINES - 2; if (mh > 30) mh = 30;
    int mw = COLS  - 4; if (mw > ICS_MSG_COLS + 6) mw = ICS_MSG_COLS + 6;
    if (mh < 26) mh = 26;
    if (mw < 60) mw = 60;
    int my = (LINES - mh) / 2; if (my < 0) my = 0;
    int mx = (COLS  - mw) / 2; if (mx < 0) mx = 0;

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    keypad(w, TRUE);
    nodelay(w, FALSE);
    curs_set(1);

    const int label_x = 2;
    const int field_x = 18;
    const int field_w = mw - field_x - 3;

    /* Row layout */
    const int y_sb       = 2;
    const int y_subject  = 3;
    const int y_agency   = 4;
    const int y_datetime = 6;
    const int y_to       = 7;
    const int y_from     = 8;
    const int y_contact  = 9;
    const int y_operators= 10;
    const int y_type     = 12;
    const int y_band     = 13;
    const int y_session  = 14;
    const int y_location = 16;
    const int y_grid     = 17;
    const int y_commentlbl = 19;
    const int y_commentbox = 20;
    int        comment_rows = mh - y_commentbox - 4;
    if (comment_rows > 6) comment_rows = 6;
    if (comment_rows < 3) comment_rows = 3;
    int        ck_ta_cols = (ICS_MSG_COLS < field_w) ? ICS_MSG_COLS : field_w;

    TextArea ta;
    ta.lines    = f.comment_lines;
    ta.nlines   = &f.comment_nlines;
    ta.cur_line = 0;
    ta.cur_col  = 0;
    ta.top_line = 0;

    int focus = CK_SB;
    int running = 1;
    int just_sent = 0;

    while (running) {
        werase(w);
        draw_box(w);
        mvwprintw(w, 0, (mw - 22) / 2, " PKTNET Packet Check-In ");

        mvwprintw(w, y_sb,        label_x, "SB Line:");
        mvwprintw(w, y_subject,   label_x, "Subject:");
        mvwprintw(w, y_agency,    label_x, "Agency/Group:");
        mvwprintw(w, y_datetime,  label_x, "1a Date/Time:");
        mvwprintw(w, y_to,        label_x, "1b To:");
        mvwprintw(w, y_from,      label_x, "1c From:");
        mvwprintw(w, y_contact,   label_x, "1d Contact:");
        mvwprintw(w, y_operators, label_x, "1e Operators:");
        mvwprintw(w, y_type,      label_x, "2a Type:");
        mvwprintw(w, y_band,      label_x, "2c Band:");
        mvwprintw(w, y_session,   label_x, "2d Session:");
        mvwprintw(w, y_location,  label_x, "3a Location:");
        mvwprintw(w, y_grid,      label_x, "3b Grid Sq:");
        mvwprintw(w, y_commentlbl, label_x, "4. Comments:");

        mvwprintw(w, y_sb,        field_x, "%-*s", field_w, f.sb_line);
        mvwprintw(w, y_subject,   field_x, "%-*s", field_w, f.subject);
        mvwprintw(w, y_agency,    field_x, "%-*s", field_w, f.agency);
        mvwprintw(w, y_datetime,  field_x, "%-*s", field_w, f.date_time);
        mvwprintw(w, y_to,        field_x, "%-*s", field_w, f.to);
        mvwprintw(w, y_from,      field_x, "%-*s", field_w, f.from);
        mvwprintw(w, y_contact,   field_x, "%-*s", field_w, f.contact_name);
        mvwprintw(w, y_operators, field_x, "%-*s", field_w, f.operators);

        if (focus == CK_TYPE)    wattron(w, A_REVERSE);
        mvwprintw(w, y_type,    field_x, "%-20s", chkin_type_opts[f.type_idx]);
        if (focus == CK_TYPE)    wattroff(w, A_REVERSE);

        mvwprintw(w, y_type, field_x + 22, "(Service: AMATEUR)");

        if (focus == CK_BAND)    wattron(w, A_REVERSE);
        mvwprintw(w, y_band,    field_x, "%-20s", chkin_band_opts[f.band_idx]);
        if (focus == CK_BAND)    wattroff(w, A_REVERSE);

        if (focus == CK_SESSION) wattron(w, A_REVERSE);
        mvwprintw(w, y_session, field_x, "%-20s", chkin_session_opts[f.session_idx]);
        if (focus == CK_SESSION) wattroff(w, A_REVERSE);

        mvwprintw(w, y_location, field_x, "%-*s", field_w, f.location);
        mvwprintw(w, y_grid,     field_x, "%-*s", field_w, f.grid);

        for (int c = label_x; c < label_x + ICS_MSG_COLS + 2 && c < mw - 1; c++) {
            mvwaddch(w, y_commentbox, c, '-');
            mvwaddch(w, y_commentbox + comment_rows + 1, c, '-');
        }
        ta_draw(w, &ta, y_commentbox + 1, label_x + 1, comment_rows,
                (ICS_MSG_COLS < field_w) ? ICS_MSG_COLS : field_w);

        mvwprintw(w, mh - 3, label_x,
                  "Tab/Up/Down move fields  Enter/Space picks on Type/Band/Session");
        mvwprintw(w, mh - 2, label_x,
                  "F2=Send   Esc=Cancel/Close");
        if (just_sent)
            mvwprintw(w, mh - 4, label_x, "Sent. Edit and F2 to send again, or Esc to close.");
        wrefresh(w);

        int ch;
        switch (focus) {
        case CK_SB:        ch = ics_edit_field(w, y_sb,       field_x, field_w, f.sb_line,      ICS_SBLINE_MAX); break;
        case CK_SUBJECT:   ch = ics_edit_field(w, y_subject,  field_x, field_w, f.subject,       CHKIN_FIELD_MAX); break;
        case CK_AGENCY:    ch = ics_edit_field(w, y_agency,   field_x, field_w, f.agency,        CHKIN_FIELD_MAX); break;
        case CK_DATETIME:  ch = ics_edit_field(w, y_datetime, field_x, field_w, f.date_time,      ICS_DT_MAX); break;
        case CK_TO:        ch = ics_edit_field(w, y_to,       field_x, field_w, f.to,             CHKIN_FIELD_MAX); break;
        case CK_FROM:      ch = ics_edit_field(w, y_from,     field_x, field_w, f.from,           CHKIN_FIELD_MAX); break;
        case CK_CONTACT:   ch = ics_edit_field(w, y_contact,  field_x, field_w, f.contact_name,   CHKIN_FIELD_MAX); break;
        case CK_OPERATORS: ch = ics_edit_field(w, y_operators,field_x, field_w, f.operators,      CHKIN_FIELD_MAX); break;
        case CK_LOCATION:  ch = ics_edit_field(w, y_location, field_x, field_w, f.location,       CHKIN_FIELD_MAX); break;
        case CK_GRID:      ch = ics_edit_field(w, y_grid,     field_x, field_w, f.grid,           CHKIN_FIELD_MAX); break;

        case CK_TYPE:
            wmove(w, y_type, field_x);
            ch = wgetch(w);
            if (ch == ' ' || ch == '\r' || ch == '\n' || ch == KEY_ENTER)
                chkin_pick("Type", chkin_type_opts, chkin_type_n, &f.type_idx);
            break;
        case CK_BAND:
            wmove(w, y_band, field_x);
            ch = wgetch(w);
            if (ch == ' ' || ch == '\r' || ch == '\n' || ch == KEY_ENTER)
                chkin_pick("Band", chkin_band_opts, chkin_band_n, &f.band_idx);
            break;
        case CK_SESSION:
            wmove(w, y_session, field_x);
            ch = wgetch(w);
            if (ch == ' ' || ch == '\r' || ch == '\n' || ch == KEY_ENTER)
                chkin_pick("Session", chkin_session_opts, chkin_session_n, &f.session_idx);
            break;

        case CK_COMMENTS:
        default: {
            wmove(w, y_commentbox + 1 + (ta.cur_line - ta.top_line), label_x + 1 + ta.cur_col);
            wrefresh(w);
            int raw = wgetch(w);
            if (raw == KEY_F(2)) { ch = KEY_F(2); break; }
            int r = ta_handle_key(&ta, raw, comment_rows, ck_ta_cols);
            if (r == '\t' || r == 27) ch = r;
            else continue;
            break;
        }
        }

        just_sent = 0;

        if (ch == KEY_F(2)) {  /* Send */
            if (chkin_send(&f)) {
                running = 0;   /* success: behave like Esc was pressed */
                break;
            }
            /* See general_message_form()'s F2 handler for why this is here. */
            redrawwin(w);
            wrefresh(w);
            just_sent = 1;      /* failed/aborted: stay open, show status */
            continue;
        }
        if (ch == 27) {  /* Esc closes the form */
            running = 0;
            break;
        }

        if (ch == '\t' || ch == KEY_DOWN || ch == '\r' || ch == '\n' || ch == KEY_ENTER) {
            focus++;
            if (focus > CK_COMMENTS) focus = CK_SB;
        } else if (ch == KEY_UP) {
            focus--;
            if (focus < CK_SB) focus = CK_COMMENTS;
        }
    }

    delwin(w);
    curs_set(1);
    force_repaint();
}

/* ═══════════════════════════════════════════════════════════
 * Session management
 * ═══════════════════════════════════════════════════════════ */

static int session_count(void)
{
    int n = 0;
    for (int i = 0; i < MAX_SESSIONS; i++)
        if (sessions[i].active) n++;
    return n;
}

static int session_index(void)   /* index of S in sessions[] */
{
    return (int)(S - sessions);
}

static void session_switch(int n)
{
    if (n < 0 || n >= MAX_SESSIONS || !sessions[n].active) return;
    S = &sessions[n];
    refresh_all();
    draw_fkeys();
}

static void session_next(void)
{
    int cur = session_index();
    for (int i = 1; i < MAX_SESSIONS; i++) {
        int n = (cur + i) % MAX_SESSIONS;
        if (sessions[n].active) { session_switch(n); return; }
    }
}

static void session_prev(void)
{
    int cur = session_index();
    for (int i = MAX_SESSIONS - 1; i > 0; i--) {
        int n = (cur + i) % MAX_SESSIONS;
        if (sessions[n].active) { session_switch(n); return; }
    }
}

static void session_new(void)
{
    int slot = -1;
    for (int i = 0; i < MAX_SESSIONS; i++)
        if (!sessions[i].active) { slot = i; break; }
    if (slot < 0) {
        append_out("Max sessions reached (4).\n", 26, COLOR_PAIR(CP_YELLOW));
        return;
    }
    int idx = pick_host("New Session — Select Host");
    if (idx < 0) return;

    memset(&sessions[slot], 0, sizeof(Session));
    sessions[slot].active   = 1;
    sessions[slot].sock     = -1;
    sessions[slot].host_idx = -1;
    sessions[slot].hist_pos = -1;
    snprintf(sessions[slot].label, SESSION_LABEL, "Session %d", slot + 1);
    S = &sessions[slot];
    refresh_all();
    draw_fkeys();
    do_connect(idx);
}

static void session_close(void)
{
    if (session_count() <= 1) {
        if (S->connected) tcp_disconnect();
        if (S->logging)   close_log();
        endwin();
        exit(0);
    }
    int idx = session_index();
    if (S->connected) tcp_disconnect();
    if (S->logging)   close_log();
    session_next();          /* S now points to another active session */
    sessions[idx].active = 0;
    draw_status();
}

/* ═══════════════════════════════════════════════════════════
 * Connect helper
 * ═══════════════════════════════════════════════════════════ */

static void do_connect(int idx)
{
    if (idx < 0 || idx >= num_hosts) return;
    if (S->connected) tcp_disconnect();

    char msg[160];
    snprintf(msg, sizeof(msg), "Connecting to %s:%s...\n",
             hosts[idx].host, hosts[idx].port);
    append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_YELLOW));

    S->sock = tcp_connect(hosts[idx].host, hosts[idx].port);
    if (S->sock < 0) {
        append_out("Connection failed.\n", 19, COLOR_PAIR(CP_RED));
        return;
    }

    S->connected    = 1;
    S->host_idx = idx;
    S->connect_time = time(NULL);

    snprintf(msg, sizeof(msg), "Connected to %s\n", hosts[idx].name);
    append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_GREEN) | A_BOLD);

    if (hosts[idx].username[0]) {
        char login[256];
        int  llen = snprintf(login, sizeof(login), "%s\r%s\rBPQTermTCP\r",
                             hosts[idx].username, hosts[idx].password);
        tcp_send(login, llen);
        char info[120];
        int  ilen = snprintf(info, sizeof(info), "[Auto-login: %s]\n",
                             hosts[idx].username);
        append_out(info, ilen, COLOR_PAIR(CP_YELLOW));
    } else {
        append_out("[No credentials - manual login required]\n", 41,
                   COLOR_PAIR(CP_YELLOW));
    }

    /* P8=1 triggers BPQ's SendPortsForMonitor → arrives as 0xFF 0xFF packet */
    send_trace_options_with_ports();

    /* Drain incoming data for up to 2 seconds waiting for PORTS response.
     * A single recv() often gets only the login banner; loop until we have
     * the full port list (S->ports_loaded) or the deadline passes. */
    {
        char   rbuf[4096];
        struct timeval t0, now;
        gettimeofday(&t0, NULL);
        for (;;) {
            gettimeofday(&now, NULL);
            long elapsed = (now.tv_sec  - t0.tv_sec)  * 1000000L
                         + (now.tv_usec - t0.tv_usec);
            if (elapsed >= 2000000L) break;
            if (S->ports_loaded)        break;
            long remain = 2000000L - elapsed;
            fd_set rfds;
            struct timeval tv = { remain / 1000000L, remain % 1000000L };
            FD_ZERO(&rfds); FD_SET(S->sock, &rfds);
            if (select(S->sock + 1, &rfds, NULL, NULL, &tv) <= 0) break;
            int n = recv(S->sock, rbuf, sizeof(rbuf) - 1, 0);
            if (n <= 0) break;
            bpq_process(rbuf, n);
        }
    }

    /* Safety-net resend: the very first send_trace_options_with_ports()
     * call above can race with BPQ still processing login - if BPQ wasn't
     * ready to parse it yet, the port mask/monitor flags silently fall
     * back to BPQ's own defaults with no error on either side (seen as
     * "port shows enabled here but no traffic flows until re-toggled").
     * Re-sending once the initial drain has settled costs one extra
     * trace command and catches that race without needing to detect it. */
    if (S->connected && S->sock >= 0) {
        send_trace_options_with_ports();
        char   rbuf[4096];
        struct timeval tv2 = { 1, 0 };
        fd_set rfds2;
        FD_ZERO(&rfds2); FD_SET(S->sock, &rfds2);
        if (select(S->sock + 1, &rfds2, NULL, NULL, &tv2) > 0) {
            int n = recv(S->sock, rbuf, sizeof(rbuf) - 1, 0);
            if (n > 0) bpq_process(rbuf, n);
        }
    }

    draw_status();
}

/* ═══════════════════════════════════════════════════════════
 * Forms submenu  (Ctrl+A -> F)
 * ═══════════════════════════════════════════════════════════ */

static void forms_menu(void)
{
    int mh = 8, mw = 40;
    int my = (LINES - mh) / 2; if (my < 0) my = 0;
    int mx = (COLS  - mw) / 2; if (mx < 0) mx = 0;

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    draw_box(w);
    mvwprintw(w, 0, (mw - 9) / 2, " Forms ");
    mvwprintw(w, 2, 4, "G - General Message (ICS-213)");
    mvwprintw(w, 3, 4, "P - PKTNET Packet Check-In");
    mvwprintw(w, mh - 2, 4, "Esc = close");
    wrefresh(w);

    nodelay(w, FALSE);
    int ch = wgetch(w);
    delwin(w);
    force_repaint();

    switch (ch | 0x20) {
    case 'g':
        general_message_form();
        break;
    case 'p':
        pktnet_checkin_form();
        break;
    }
}

/* ═══════════════════════════════════════════════════════════
 * Ctrl+A main menu
 * ═══════════════════════════════════════════════════════════ */

static void ctrl_a_menu(void)
{
    int mh = 15, mw = 42;
    int my = (LINES-mh)/2; if (my<0) my=0;
    int mx = (COLS-mw)/2;  if (mx<0) mx=0;

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    draw_box(w);
    mvwprintw(w, 0, (mw-14)/2, " TermTCP Menu ");
    mvwprintw(w, 2, 4, "C - Connect");
    mvwprintw(w, 3, 4, "D - Disconnect");
    mvwprintw(w, 4, 4, "N - New Session");
    mvwprintw(w, 5, 4, "X - Close Session");
    mvwprintw(w, 6, 4, "1-4 Switch Session");
    mvwprintw(w, 7, 4, "O - Config / Host Editor");
    mvwprintw(w, 8, 4, "M - Monitor Options");
    mvwprintw(w, 9, 4, "L - Toggle Log");
    mvwprintw(w, 10, 4, "F - Forms & Check-Ins");
    mvwprintw(w, 11, 4, "H - Help");
    mvwprintw(w, 12, 4, "Q - Quit");
    mvwprintw(w, mh-2, 4, "Esc = close");
    wrefresh(w);

    nodelay(w, FALSE);
    int ch = wgetch(w);
    delwin(w);
    force_repaint();

    switch (ch | 0x20) {
    case 'c': {
        int idx = pick_host("Select Host");
        if (idx >= 0) do_connect(idx);
        break;
    }
        break;

    case 'd':
        if (S->connected) {
            tcp_disconnect();
            append_out("*** Disconnected\n", 17, COLOR_PAIR(CP_RED));
        }
        break;

    case 'o':
        config_menu();
        break;

    case 'm':
        monitor_options_menu();
        break;

    case 'l':
        if (S->logging) {
            close_log();
            append_out("[Logging OFF]\n", 14, COLOR_PAIR(CP_YELLOW));
        } else if (S->connected && S->host_idx >= 0) {
            char prefix[120];
            snprintf(prefix, sizeof(prefix), "%s_%s",
                     hosts[S->host_idx].host, hosts[S->host_idx].port);
            open_log(prefix);
            append_out("[Logging ON]\n", 13, COLOR_PAIR(CP_YELLOW));
        } else {
            append_out("Connect first to start S->logging.\n", 32, COLOR_PAIR(CP_YELLOW));
        }
        draw_status();
        break;

    case 'f':
        forms_menu();
        break;

    case 'h':
        show_help();
        break;

    case 'n':
        session_new();
        break;

    case 'x':
        session_close();
        break;

    case '1': case '2': case '3': case '4':
        session_switch(ch - '1');
        break;

    case 'q':
        if (S->connected) tcp_disconnect();
        if (S->logging)   close_log();
        endwin();
        exit(0);
    }

    draw_input();
}

/* ═══════════════════════════════════════════════════════════
 * Key handler
 * ═══════════════════════════════════════════════════════════ */

static void handle_key(int ch)
{
    switch (ch) {
    case 1:  /* Ctrl+A */
        ctrl_a_menu();
        return;

    case KEY_F(1):
        show_help();
        return;

    case KEY_F(3): {
        int idx = pick_host("Select Host");
        if (idx >= 0) do_connect(idx);
        return;
    }

    case KEY_F(4):
        if (S->connected) {
            tcp_disconnect();
            append_out("*** Disconnected\n", 17, COLOR_PAIR(CP_RED));
            draw_status();
        }
        return;

    case KEY_F(5):
        forms_menu();
        return;

    case KEY_F(6):
        if (S->logging) {
            close_log();
            append_out("[Logging OFF]\n", 14, COLOR_PAIR(CP_YELLOW));
        } else if (S->connected && S->host_idx >= 0) {
            char prefix[120];
            snprintf(prefix, sizeof(prefix), "%s_%s",
                     hosts[S->host_idx].host, hosts[S->host_idx].port);
            open_log(prefix);
            append_out("[Logging ON]\n", 13, COLOR_PAIR(CP_YELLOW));
        } else {
            append_out("Connect first to start S->logging.\n", 32, COLOR_PAIR(CP_YELLOW));
        }
        draw_status();
        return;

    case KEY_F(7):
        session_new();
        return;

    case KEY_F(8):
        session_prev();
        return;

    case KEY_F(9):
        session_next();
        return;

    case KEY_F(10):
        if (S->connected) tcp_disconnect();
        if (S->logging)   close_log();
        endwin();
        exit(0);

    case KEY_RESIZE:
        endwin(); refresh(); clear();
        create_windows();
        refresh_all();
        return;

    case KEY_PPAGE:  /* PgUp — scroll output back */
        if (out_win) {
            int page = getmaxy(out_win);
            S->out_scroll += page;
            int max_scroll = S->out_sbuf_count > 0 ? S->out_sbuf_count - 1 : 0;
            if (S->out_scroll > max_scroll) S->out_scroll = max_scroll;
            scrl_redraw_out();
            update_sep_labels();
            if (input_win) wrefresh(input_win);
        }
        return;

    case KEY_NPAGE:  /* PgDn — scroll output forward / return to live */
        if (out_win && S->out_scroll > 0) {
            int page = getmaxy(out_win);
            S->out_scroll -= page;
            if (S->out_scroll < 0) S->out_scroll = 0;
            scrl_redraw_out();
            update_sep_labels();
            if (input_win) wrefresh(input_win);
        }
        return;

    case KEY_HOME:  /* Home — scroll monitor back */
        if (mon_win && mon_visible) {
            int page = getmaxy(mon_win);
            S->mon_scroll += page;
            int max_scroll = S->mon_sbuf_count > 0 ? S->mon_sbuf_count - 1 : 0;
            if (S->mon_scroll > max_scroll) S->mon_scroll = max_scroll;
            scrl_redraw_mon();
            if (input_win) wrefresh(input_win);
        }
        return;

    case KEY_END:  /* End — scroll monitor forward / return to live */
        if (mon_win && mon_visible && S->mon_scroll > 0) {
            int page = getmaxy(mon_win);
            S->mon_scroll -= page;
            if (S->mon_scroll < 0) S->mon_scroll = 0;
            scrl_redraw_mon();
            if (input_win) wrefresh(input_win);
        }
        return;

    case '\r': case '\n': case KEY_ENTER:
        if (S->input_len == 0) return;
        S->input_buf[S->input_len] = '\0';
        /* Save to history */
        strncpy(S->hist_buf[S->hist_head], S->input_buf, INPUT_MAX - 1);
        S->hist_buf[S->hist_head][INPUT_MAX - 1] = '\0';
        S->hist_head  = (S->hist_head + 1) % HIST_MAX;
        if (S->hist_count < HIST_MAX) S->hist_count++;
        S->hist_pos = -1;
        if (S->connected) {
            tcp_send(S->input_buf, S->input_len);
            tcp_send("\r", 1);
            char echo[INPUT_MAX + 4];
            int  elen = snprintf(echo, sizeof(echo), "> %s\n", S->input_buf);
            append_out(echo, elen, COLOR_PAIR(CP_CYAN) | A_BOLD);
        } else {
            append_out("Not S->connected -- Ctrl+A to open menu.\n", 38,
                       COLOR_PAIR(CP_YELLOW));
        }
        S->input_len = 0;
        break;

    case KEY_UP:  /* history: go back */
        if (S->hist_count == 0) break;
        if (S->hist_pos == -1) {
            strncpy(S->hist_save, S->input_buf, INPUT_MAX - 1);
            S->hist_save[INPUT_MAX - 1] = '\0';
            S->hist_pos = 0;
        } else if (S->hist_pos < S->hist_count - 1) {
            S->hist_pos++;
        }
        {
            int idx = (S->hist_head - 1 - S->hist_pos + HIST_MAX * 2) % HIST_MAX;
            strncpy(S->input_buf, S->hist_buf[idx], INPUT_MAX - 1);
            S->input_buf[INPUT_MAX - 1] = '\0';
            S->input_len = (int)strlen(S->input_buf);
        }
        break;

    case KEY_DOWN:  /* history: go forward */
        if (S->hist_pos < 0) break;
        S->hist_pos--;
        if (S->hist_pos < 0) {
            S->input_buf[0] = '\0';   /* always end with blank line */
        } else {
            int idx = (S->hist_head - 1 - S->hist_pos + HIST_MAX * 2) % HIST_MAX;
            strncpy(S->input_buf, S->hist_buf[idx], INPUT_MAX - 1);
            S->input_buf[INPUT_MAX - 1] = '\0';
        }
        S->input_len = (int)strlen(S->input_buf);
        break;

    case KEY_BACKSPACE: case 127: case '\b':
        if (S->input_len > 0) S->input_len--;
        break;

    default:
        if (ch >= 32 && ch < 256 && S->input_len < INPUT_MAX - 1)
            S->input_buf[S->input_len++] = (char)ch;
        break;
    }

    draw_input();
}

/* ═══════════════════════════════════════════════════════════
 * Main event loop
 * ═══════════════════════════════════════════════════════════ */

/* Shown when Ctrl+C (SIGINT) is caught. Returns 1 if the user
 * confirms quitting, 0 to resume exactly where they were. */
static int confirm_quit_popup(void)
{
    int mh = 7, mw = 40;
    int my = (LINES - mh) / 2; if (my < 0) my = 0;
    int mx = (COLS  - mw) / 2; if (mx < 0) mx = 0;

    WINDOW *w = newwin(mh, mw, my, mx);
    wbkgd(w, COLOR_PAIR(CP_DEFAULT));
    draw_box(w);
    mvwprintw(w, 0, (mw - 8) / 2, " Quit? ");
    mvwprintw(w, 2, 3, "Ctrl+C pressed.");
    if (S->connected) mvwprintw(w, 3, 3, "This will close the current session.");
    mvwprintw(w, mh - 2, 3, "Y = quit    N / Esc = resume");
    wrefresh(w);

    nodelay(w, FALSE);
    keypad(w, TRUE);
    int ch = wgetch(w);
    delwin(w);
    force_repaint();

    return (ch == 'y' || ch == 'Y');
}

void interactive_loop(void)
{
    fd_set readfds;
    struct timeval tv;
    char recv_buf[1024];

    draw_input();

    while (1) {
        if (sigint_pending) {
            sigint_pending = 0;
            if (confirm_quit_popup()) {
                if (S->connected) tcp_disconnect();
                if (S->logging)   close_log();
                endwin();
                exit(0);
            }
            draw_input();
        }

        draw_status();

        /* Watch stdin + every connected session's socket */
        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds);
        int maxfd = STDIN_FILENO;
        for (int i = 0; i < MAX_SESSIONS; i++) {
            if (sessions[i].active && sessions[i].connected && sessions[i].sock >= 0) {
                FD_SET(sessions[i].sock, &readfds);
                if (sessions[i].sock > maxfd) maxfd = sessions[i].sock;
            }
        }

        tv.tv_sec  = 1;
        tv.tv_usec = 0;
        int ready = select(maxfd + 1, &readfds, NULL, NULL, &tv);
        if (ready < 0) continue;

        /* Network data — process all active sessions */
        Session *active = S;
        for (int i = 0; i < MAX_SESSIONS; i++) {
            if (!sessions[i].active || !sessions[i].connected || sessions[i].sock < 0)
                continue;
            if (!FD_ISSET(sessions[i].sock, &readfds))
                continue;

            int is_bg = (&sessions[i] != active);
            S = &sessions[i];
            if (is_bg) suppress_draw = 1;

            int n = recv(S->sock, recv_buf, sizeof(recv_buf)-1, 0);
            if (n > 0) {
                bpq_process(recv_buf, n);
                if (!is_bg) draw_input();
            } else if (n == 0 || (n < 0 && errno != EAGAIN)) {
                append_out("\n*** Server closed connection ***\n", 33,
                           COLOR_PAIR(CP_RED));
                tcp_disconnect();
            }

            suppress_draw = 0;
            S = active;
        }

        /* Keyboard */
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            int k;
            while ((k = wgetch(input_win)) != ERR)
                handle_key(k);
        }
    }
}

/* ═══════════════════════════════════════════════════════════
 * main()
 * ═══════════════════════════════════════════════════════════ */

int main(int argc, char *argv[])
{
    int auto_connect = -1;

    for (int i = 1; i < argc; i++) {
        if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--connect") == 0)
            && i+1 < argc)
            auto_connect = atoi(argv[++i]);
        else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
            printf("Usage: termtcp [-c N]\n  -c N  auto-connect to host N\n");
            return 0;
        }
    }

    /* Initialise session 0 — the one and only session in CLI mode */
    memset(sessions, 0, sizeof(sessions));
    sessions[0].active   = 1;
    sessions[0].sock     = -1;
    sessions[0].host_idx = -1;
    sessions[0].hist_pos = -1;
    snprintf(sessions[0].label, SESSION_LABEL, "Session 1");
    S = &sessions[0];

    /* Set default port names before load_config so INI PortName keys override them */
    for (int i = 0; i < 32; i++)
        snprintf(port_names[i], 32, "Port %d", i + 1);

    load_config();

    setlocale(LC_ALL, "");   /* enable UTF-8 so ncursesw renders box chars correctly */
    initscr();
    cbreak();
    noecho();
    set_escdelay(25);   /* fast Escape key response */
    init_colors();
    curs_set(1);
    create_windows();
    draw_status();

    {
        /* Trap Ctrl+C so it asks for confirmation instead of killing the
         * program outright. SA_RESTART deliberately omitted: select() in
         * interactive_loop() should return EINTR so the pending flag is
         * noticed on the very next loop iteration rather than after the
         * current 1-second poll silently restarts. */
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = on_sigint;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = 0;
        sigaction(SIGINT, &sa, NULL);
    }

    {
        const char *banner = "TermTCP v" VERSION " - Ctrl+A opens the menu\n";
        append_out(banner, (int)strlen(banner), COLOR_PAIR(CP_CYAN) | A_BOLD);
    }

    if (num_hosts > 0) {
        char msg[128];
        snprintf(msg, sizeof(msg),
                 "Loaded %d host(s). Ctrl+A then C to connect.\n", num_hosts);
        append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_DEFAULT));
    } else {
        append_out("No hosts configured. Ctrl+A then O to edit config.\n", 51,
                   COLOR_PAIR(CP_YELLOW));
    }

    if (auto_connect >= 0) do_connect(auto_connect);

    interactive_loop();

    if (S->connected) tcp_disconnect();
    if (S->logging)   close_log();
    endwin();
    return 0;
}
