/*
 * helpers.c — Portable C helpers for zyginit
 *
 * Thin wrappers around POSIX functions that need:
 *   - const char* bridging for Reef FFI (which emits char*)
 *   - multi-step sequences grouped for atomicity (e.g. drop_privileges)
 *   - passwd/group lookups returning simple int values
 *
 * Compiled on both Linux and Hammerhead. On Hammerhead it is passed alongside
 * -lcontract; on Linux it is passed alongside contract_linux_stubs.o.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <time.h>
#include <utmpx.h>
#include <errno.h>
#include <sys/types.h>

#ifdef __sun
#include <sys/uadmin.h>
#include <stropts.h>
#endif

/* Wrapper for symlink() — avoids const char* vs char* conflict with Reef FFI */
int zyginit_symlink(char *target, char *linkpath) {
    return symlink(target, linkpath);
}

/* Wrapper for rename() — avoids const char* vs char* conflict with Reef FFI */
int zyginit_rename(char *oldpath, char *newpath) {
    return rename(oldpath, newpath);
}

/* Wrapper for unlink() — avoids const char* vs char* conflict with Reef FFI */
int zyginit_unlink(char *path) {
    return unlink(path);
}

/* Wrapper for fsync() — sys.fd does not expose fsync; the wrapper keeps
 * the FFI surface consistent with zyginit_rename / zyginit_unlink. */
int zyginit_fsync(int fd) {
    return fsync(fd);
}

/* Look up a user by name. Returns uid, or -1 if not found. */
int zyginit_getuid_by_name(char *name) {
    struct passwd *pw = getpwnam(name);
    if (pw == NULL) return -1;
    return (int)pw->pw_uid;
}

/* Look up a user's primary gid by name. Returns gid, or -1 if not found. */
int zyginit_getgid_by_username(char *name) {
    struct passwd *pw = getpwnam(name);
    if (pw == NULL) return -1;
    return (int)pw->pw_gid;
}

/* Look up a group by name. Returns gid, or -1 if not found. */
int zyginit_getgid_by_name(char *name) {
    struct group *gr = getgrnam(name);
    if (gr == NULL) return -1;
    return (int)gr->gr_gid;
}

/* Drop privileges: setgid, initgroups, setuid. Returns 0 on success, -1 on error. */
int zyginit_drop_privileges(char *username, int uid, int gid) {
    if (setgid(gid) != 0) return -1;
    if (initgroups(username, gid) != 0) return -1;
    if (setuid(uid) != 0) return -1;
    return 0;
}

/* Flush all dirty page-cache buffers to disk before shutdown. sync(2) is
 * POSIX and exists on both Linux and Hammerhead — no #ifdef needed.
 *
 * NOTE: sync(2) on illumos *schedules* writes; it does not wait for
 * completion. For ZFS with pending metadata changes (e.g. our
 * destroy_socket's unlink), use zyginit_fsync_path() to force a
 * synchronous commit. */
void zyginit_sync(void) {
    sync();
}

/* Run a shell command via system(3), waiting for completion. Used in
 * the shutdown path to force `zpool sync` and `umountall -l` before
 * uadmin so all pending ZFS metadata (especially destroy_socket's
 * unlink) is durably on disk. fsync alone isn't enough — illumos's
 * sync(2) is async and the kernel reboot races with the txg commit.
 *
 * Returns the command's exit status, or -1 if system() itself failed. */
int zyginit_run_cmd(char *cmd) {
    int rc = system(cmd);
    if (rc < 0) return -1;
    return rc;
}

/* fsync the file at the given path (or its parent dir if the path
 * itself doesn't exist). Forces ZFS txg commit for any pending changes
 * touching that path. Returns 0 on success, -1 on error.
 *
 * Used after destroy_socket's unlink(/var/run/zyginit.sock) to make
 * sure the unlink is on disk before the child process triggers
 * uadmin. Without it, the kernel reboot can race with the ZFS txg
 * commit — boot 2 then sees a stale socket file in /var/run, boot 2's
 * create_socket can't recover cleanly, and zygctl loses its socket. */
#include <fcntl.h>
#include <libgen.h>
int zyginit_fsync_path(char *path) {
    int fd;
    fd = open(path, O_RDONLY);
    if (fd < 0) {
        /* Path doesn't exist (probably just unlinked). fsync the parent
         * dir so the directory entry's removal is committed. */
        char buf[4096];
        char *parent;
        strncpy(buf, path, sizeof(buf) - 1);
        buf[sizeof(buf) - 1] = '\0';
        parent = dirname(buf);
        fd = open(parent, O_RDONLY);
        if (fd < 0) return -1;
    }
    int rc = fsync(fd);
    int saved = errno;
    close(fd);
    errno = saved;
    return rc;
}

/* Shutdown wrapper — uadmin(A_SHUTDOWN, fcn, 0). On Hammerhead this is the
 * primitive init uses to halt, reboot, or poweroff the system. fcn is one of:
 *   AD_HALT     (0) — stop scheduler, wait for operator
 *   AD_BOOT     (1) — reboot
 *   AD_POWEROFF (6) — ACPI/BMC chassis off
 * Returns 0 on success, -1 on error (errno set). On success, does not return
 * for AD_BOOT / AD_POWEROFF; for AD_HALT the kernel enters a halted state.
 * On Linux (dev build only) returns -1 unconditionally.
 */
#ifdef __sun
int zyginit_shutdown(int fcn) {
    return uadmin(A_SHUTDOWN, fcn, 0);
}
#else
int zyginit_shutdown(int fcn) { (void)fcn; return -1; }
#endif

/* Push the ldterm + ttcompat STREAMS modules onto an open fd so that
 * \n is translated to \r\n on output. Without this, zyginit's println
 * output to /dev/console (which has no autopush'd line discipline at
 * the time PID 1 first opens it) appears column-shifted relative to
 * kernel cmn_err writes. Returns 0 on success, -1 on any push error.
 * Linux build is a no-op (STREAMS doesn't exist there). */
#ifdef __sun
int zyginit_push_ldterm(int fd) {
    if (ioctl(fd, I_PUSH, "ldterm") < 0) return -1;
    if (ioctl(fd, I_PUSH, "ttcompat") < 0) return -1;
    return 0;
}
#else
int zyginit_push_ldterm(int fd) { (void)fd; return 0; }
#endif

/* Write a BOOT_TIME utmpx record. Standard illumos init does this so
 * who(1) -b and uptime(1) report the actual boot time. Returns 0 on
 * success, -1 on error. */
int zyginit_write_boot_utmpx(void) {
    struct utmpx ux;
    memset(&ux, 0, sizeof (ux));
    strncpy(ux.ut_user, "reboot", sizeof (ux.ut_user));
    strncpy(ux.ut_line, "system boot", sizeof (ux.ut_line));
    ux.ut_pid = 0;
    ux.ut_type = BOOT_TIME;
    (void) time(&ux.ut_tv.tv_sec);
    setutxent();
    if (pututxline(&ux) == NULL) { endutxent(); return -1; }
    endutxent();
    return 0;
}

/* Write a RUN_LVL utmpx record with the given level character (e.g.,
 * 'S' for single-user, '3' for multi-user). who(1) -r and runlevel(8)
 * read this. Returns 0 on success, -1 on error.
 *
 * Format matches what svc.startd's utmpx_set_runlevel() writes
 * (cmd/svc/startd/utmpx.c):
 *   ut_line = "run-level X" (where X is the level char)
 *   ut_exit.e_termination = new runlevel char
 *   ut_exit.e_exit = previous level (we use 0 — first transition)
 *   ut_type = RUN_LVL
 *   ut_id, ut_user, ut_pid = unused for run-level records
 *
 * Linux build is a no-op (RUN_LVL constant and the e_termination /
 * e_exit struct members don't exist in glibc's utmpx; this is a
 * Hammerhead-specific feature for who(1)/uptime(1) integration).
 */
#ifdef __sun
#include <stdio.h>
int zyginit_write_runlvl_utmpx(int level_char) {
    struct utmpx ux;
    struct utmpx *oup;
    char prev_level = '0';

    memset(&ux, 0, sizeof (ux));
    ux.ut_type = RUN_LVL;

    setutxent();
    /* For RUN_LVL/BOOT_TIME, getutxid matches on type only — use it
     * to find the existing record so pututxline updates rather than
     * appends. Without this, who(1) -r returns the FIRST record
     * (often stale) instead of the most-recent transition. */
    oup = getutxid(&ux);
    if (oup != NULL) {
        prev_level = oup->ut_exit.e_termination;
    }

    snprintf(ux.ut_line, sizeof (ux.ut_line), "run-level %c", (char)level_char);
    ux.ut_exit.e_termination = (char)level_char;
    ux.ut_exit.e_exit = prev_level;
    (void) time(&ux.ut_tv.tv_sec);

    if (pututxline(&ux) == NULL) { endutxent(); return -1; }
    endutxent();
    return 0;
}
#else
int zyginit_write_runlvl_utmpx(int level_char) { (void)level_char; return -1; }
#endif

/* Read the BOOT_TIME utmpx record's tv_sec value, used as the boot-id
 * sentinel for the live-replace state file. PID-1 in-place execve
 * preserves the same proc_t but the kernel does not preserve a "boot
 * id" anywhere obvious — utmpx BOOT_TIME survives because it lives in
 * /var/adm/utmpx, written by zyginit_write_boot_utmpx() at boot. A
 * real reboot writes a new BOOT_TIME with a different tv_sec, so
 * comparing against it lets the new zyginit detect a stale state file.
 *
 * Returns the BOOT_TIME tv_sec on success, or -1 if the file is not
 * readable, has no BOOT_TIME entry, or any utmpx call fails.
 *
 * NOTE: This function is no longer used by read_boot_time() in replace.reef
 * (replaced by zyginit_read_pid1_start below), but is retained here for
 * any future use (e.g., updating utmpx BOOT_TIME on detected reboot).
 */
int zyginit_read_boot_time(void) {
    struct utmpx *up;
    int result = -1;

    setutxent();
    while ((up = getutxent()) != NULL) {
        if (up->ut_type == BOOT_TIME) {
            result = (int) up->ut_tv.tv_sec;
            break;
        }
    }
    endutxent();
    return result;
}

/* Return CLOCK_MONOTONIC time in milliseconds as an int.
 * Used by ui.reef for elapsed-time stamps on tape rows.
 * Returns 0 on error (no caller needs error detail). */
int zyginit_monotonic_ms(void) {
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
    return (int)((ts.tv_sec * 1000) + (ts.tv_nsec / 1000000));
}

/* Rolling tape backing store for ui.reef.
 *
 * Reef's module-level `new []` initializers compile to GNU statement
 * expressions, which are invalid in C static initializer context.
 * Using pre-allocated C static arrays avoids the limitation entirely.
 *
 * String slots (glyph/name/note) are owned copies (strdup'd). Reef GC
 * may collect the original strings before a deferred redraw (Task 6
 * ui_tick) reads them back, so we take ownership here.
 */
/* ZYGINIT_TAPE_SLOTS is the maximum physical tape buffer size.
 * The *active* tape height is computed at runtime by ui_detect_winsize()
 * (stored in g_tape_height, clamped to [4, 32]) so it adapts to the
 * terminal size.  The C buffer is sized to the maximum so the ring-buffer
 * logic can use any slot index 0..31 without bounds trouble; rows beyond
 * g_tape_height are simply never used. */
#define ZYGINIT_TAPE_SLOTS 32

static int   zyginit_tape_elapsed_buf[ZYGINIT_TAPE_SLOTS];
static char *zyginit_tape_glyph_buf[ZYGINIT_TAPE_SLOTS];
static char *zyginit_tape_name_buf[ZYGINIT_TAPE_SLOTS];
static char *zyginit_tape_note_buf[ZYGINIT_TAPE_SLOTS];
static int   zyginit_tape_active_buf[ZYGINIT_TAPE_SLOTS];

/* Return the number of tape slots. Single source of truth for ui.reef. */
int zyginit_tape_slots(void) {
    return ZYGINIT_TAPE_SLOTS;
}

void zyginit_tape_clear(void) {
    int i;
    for (i = 0; i < ZYGINIT_TAPE_SLOTS; i++) {
        zyginit_tape_elapsed_buf[i] = 0;
        free(zyginit_tape_glyph_buf[i]); zyginit_tape_glyph_buf[i] = NULL;
        free(zyginit_tape_name_buf[i]);  zyginit_tape_name_buf[i]  = NULL;
        free(zyginit_tape_note_buf[i]);  zyginit_tape_note_buf[i]  = NULL;
        zyginit_tape_active_buf[i] = 0;
    }
}

/* zyginit_tape_set: set all fields of a slot, including the active flag.
 * active: 0 = static, 1 = forward spinner (starting), 2 = reverse spinner (stopping). */
void zyginit_tape_set(int slot, int elapsed, char *glyph, char *name, char *note, int active) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return;
    zyginit_tape_elapsed_buf[slot] = elapsed;

    free(zyginit_tape_glyph_buf[slot]);
    zyginit_tape_glyph_buf[slot] = (glyph && glyph[0]) ? strdup(glyph) : NULL;

    free(zyginit_tape_name_buf[slot]);
    zyginit_tape_name_buf[slot]  = (name  && name[0])  ? strdup(name)  : NULL;

    free(zyginit_tape_note_buf[slot]);
    zyginit_tape_note_buf[slot]  = (note  && note[0])  ? strdup(note)  : NULL;

    zyginit_tape_active_buf[slot] = active;
}

int zyginit_tape_get_active(int slot) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return 0;
    return zyginit_tape_active_buf[slot];
}

void zyginit_tape_set_active(int slot, int active) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return;
    zyginit_tape_active_buf[slot] = active;
}

/* Update the glyph in place — used when a row transitions from active to terminal. */
void zyginit_tape_set_glyph(int slot, char *glyph) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return;
    free(zyginit_tape_glyph_buf[slot]);
    zyginit_tape_glyph_buf[slot] = (glyph && glyph[0]) ? strdup(glyph) : NULL;
}

/* Update the note in place. */
void zyginit_tape_set_note(int slot, char *note) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return;
    free(zyginit_tape_note_buf[slot]);
    zyginit_tape_note_buf[slot] = (note && note[0]) ? strdup(note) : NULL;
}

int   zyginit_tape_get_elapsed(int slot) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return 0;
    return zyginit_tape_elapsed_buf[slot];
}
char *zyginit_tape_get_glyph(int slot) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return (char *)"";
    return zyginit_tape_glyph_buf[slot] ? zyginit_tape_glyph_buf[slot] : (char *)"";
}
char *zyginit_tape_get_name(int slot) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return (char *)"";
    return zyginit_tape_name_buf[slot]  ? zyginit_tape_name_buf[slot]  : (char *)"";
}
char *zyginit_tape_get_note(int slot) {
    if (slot < 0 || slot >= ZYGINIT_TAPE_SLOTS) return (char *)"";
    return zyginit_tape_note_buf[slot]  ? zyginit_tape_note_buf[slot]  : (char *)"";
}

/* zyginit_isatty: thin wrapper around isatty(3) so Reef FFI sees an int
 * return. Returns 1 if fd is a terminal, 0 otherwise. */
int zyginit_isatty(int fd) {
    return isatty(fd) ? 1 : 0;
}

/* zyginit_esc_str: returns a static string containing only the ESC
 * character (0x1b). Used by ui.reef to build ANSI/SGR escape sequences
 * at runtime, since the Reef lexer does not support \x or octal string
 * escapes — only \n, \t, \r, \\, \", \$. */
const char *zyginit_esc_str(void) {
    static const char buf[2] = { '\x1b', '\0' };
    return buf;
}

/* Read terminal size from fd (usually 1 = stdout = /dev/console).
 * Stores rows in out_rows[0] and cols in out_cols[0].
 * Returns 0 on success, -1 on failure.
 *
 * On Hammerhead /dev/console is a STREAMS tty that supports TIOCGWINSZ
 * via the tem driver's GWINSZ ioctl. On Linux (dev build) this works
 * against any real tty but falls back to -1 when not a tty (e.g. in
 * integration tests), which is fine — ui_detect_winsize uses the
 * ZYGINIT_FORCE_80x25 path for test stability. */
#include <sys/ioctl.h>
int zyginit_get_winsize(int fd, int *out_rows, int *out_cols) {
    struct winsize ws;
    if (ioctl(fd, TIOCGWINSZ, &ws) != 0) return -1;
    *out_rows = ws.ws_row;
    *out_cols = ws.ws_col;
    return 0;
}

/* Read PID 1's process start time, used as the boot-id sentinel for
 * live-replace. Survives execve (proc_t preserved) but changes on real
 * reboot (new PID 1). Returns the start time as an int, or -1 on error.
 *
 * Linux: /proc/1/stat field 22 (start_time in jiffies-since-boot)
 * illumos: /proc/1/psinfo pr_start.tv_sec (Unix timestamp)
 *
 * This correctly handles the case where zyginit_write_boot_utmpx() is
 * called at startup by the new zyginit after execve — pututxline updates
 * rather than appends the BOOT_TIME record, so utmpx BOOT_TIME is
 * unreliable as a boot-id sentinel after live replace. /proc/1/start
 * is stable across execve but changes on real reboot (new PID 1 starts
 * with a fresh proc_t), making it the correct sentinel.
 */
#ifdef __sun
#include <procfs.h>
int zyginit_read_pid1_start(void) {
    int fd;
    psinfo_t psinfo;
    fd = open("/proc/1/psinfo", O_RDONLY);
    if (fd < 0) return -1;
    if (read(fd, &psinfo, sizeof(psinfo)) != (ssize_t)sizeof(psinfo)) {
        close(fd);
        return -1;
    }
    close(fd);
    return (int) psinfo.pr_start.tv_sec;
}
#else
int zyginit_read_pid1_start(void) {
    int fd;
    char buf[1024];
    int n;
    fd = open("/proc/1/stat", O_RDONLY);
    if (fd < 0) return -1;
    n = read(fd, buf, sizeof(buf) - 1);
    close(fd);
    if (n <= 0) return -1;
    buf[n] = '\0';

    /* /proc/<pid>/stat format: "pid (comm) state ppid pgrp ..."
     * Field 22 (1-indexed) is starttime. Parse by skipping past the
     * comm field's trailing ')', then count whitespace-separated fields.
     * Skip to the LAST ')' to handle commas/parens in comm. */
    char *p = strrchr(buf, ')');
    if (p == NULL) return -1;
    p++;  /* skip past ')' */

    /* Now we're at " state ppid pgrp ... starttime ..."
     * Field numbering: pid=1, comm=2, state=3, ppid=4, ..., starttime=22.
     * We have already consumed fields 1 and 2 (pid + comm). We need to
     * skip fields 3..21 (19 fields) to land on field 22 (starttime).
     * Loop while field < 21 skips 19 tokens (field=2..20, 19 iterations). */
    int field = 2;  /* we just consumed pid + comm = fields 1,2 */
    while (*p && field < 21) {
        while (*p == ' ') p++;
        while (*p && *p != ' ') p++;
        field++;
    }
    while (*p == ' ') p++;
    if (*p == '\0') return -1;

    long starttime = 0;
    while (*p >= '0' && *p <= '9') {
        starttime = starttime * 10 + (*p - '0');
        p++;
    }
    return (int) starttime;
}
#endif
