/* signal.c — single-threaded WebRTC room-code signaling server.
 *
 * Endpoints (all bodies opaque to the server):
 *   POST   /room/<code>/offer     store offer, wake any waiters
 *   GET    /room/<code>/offer     return offer (long-poll up to 10 s, 204 on timeout)
 *   POST   /room/<code>/answer    store answer, wake any waiters
 *   GET    /room/<code>/answer    return answer (long-poll up to 10 s, 204 on timeout)
 *   OPTIONS *                     CORS preflight
 *
 * Build:  cc -O2 signal.c -o signal
 * Run:    ./signal [port]      (defaults to 8080)
 *
 * The whole server runs on one thread with poll(). Connections are state
 * machines (READ → WAIT → WRITE → done). A waiting GET is parked with no
 * IO interest until a matching POST flips it to WRITE. The long-poll
 * timeout returns 204 No Content
 *
 * Portability: strict POSIX.1-2008
 */

#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <limits.h>
#include <time.h>
#include <signal.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/resource.h>
#include <netinet/in.h>

#define MAX_ROOMS      1024
#define MAX_CONNS       (MAX_ROOMS * 2)  /* worst case: both peers parked on the same room */
#define MAX_BLOB       (64 * 1024)
#define MAX_HEADER      4096
#define RBUF_SIZE       (MAX_HEADER + MAX_BLOB)
#define ROOM_TTL_S       300
#define GC_INTERVAL_S     60
#define LONGPOLL_S        10
#define IDLE_TIMEOUT_S    15
#define CODE_MAX          15
#define METHOD_MAX         7
#define PATH_MAX_LEN      79

enum cstate {
    C_FREE = 0,
    C_READ,    /* reading request headers + body */
    C_WAIT,    /* long-polling for a slot to fill */
    C_WRITE,   /* sending response */
};

struct conn {
    int             fd;
    enum cstate     state;
    time_t          deadline;     /* absolute time after which this conn is cleaned up */

    char           *rbuf;         /* allocated lazily on first read, freed on release */
    size_t          rlen;
    size_t          body_off;     /* offset of body start in rbuf (0 = headers not parsed yet) */
    size_t          body_want;    /* Content-Length */

    char            method[METHOD_MAX + 1];
    char            path[PATH_MAX_LEN + 1];

    char           *wbuf;
    size_t          wlen, woff;

    int             wait_room;    /* index into rooms[] when C_WAIT, else -1 */
    char            wait_slot;    /* 'o' or 'a' when C_WAIT */
};

struct room {
    char    code[CODE_MAX + 1];
    char   *offer;   size_t offer_n;
    char   *answer;  size_t answer_n;
    time_t  touched;
};

static struct conn conns[MAX_CONNS];
static struct room rooms[MAX_ROOMS];
static int         server_fd = -1;
static volatile sig_atomic_t stop_flag = 0;

/* ------------------------------------------------------------------------ */

static void on_stop(int sig) { (void)sig; stop_flag = 1; }

/* Monotonic seconds — immune to NTP steps and manual wall-clock changes. */
static time_t monotonic_now(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec;
}

/* Case-insensitive substring search. Replaces the GNU strcasestr() so the
 * file compiles under strict POSIX (-D_POSIX_C_SOURCE=200809L, no GNU). */
static char *ci_strstr(const char *hay, const char *needle) {
    size_t nlen = strlen(needle);
    if (nlen == 0) return (char *)hay;
    for (; *hay; hay++) {
        if (strncasecmp(hay, needle, nlen) == 0) return (char *)hay;
    }
    return NULL;
}

static const char *status_text(int s) {
    switch (s) {
    case 200: return "OK";
    case 204: return "No Content";
    case 400: return "Bad Request";
    case 404: return "Not Found";
    case 405: return "Method Not Allowed";
    case 408: return "Request Timeout";
    case 413: return "Payload Too Large";
    case 414: return "URI Too Long";
    case 503: return "Service Unavailable";
    default:  return "Error";
    }
}

/* Returns 0 on success, -1 on failure. We bail rather than continue with a
 * blocking socket: in the accept loop a blocking fd would freeze the server
 * on the second accept after the kernel's queue is drained. */
static int set_nonblock(int fd) {
    int f = fcntl(fd, F_GETFL, 0);
    if (f < 0) return -1;
    return fcntl(fd, F_SETFL, f | O_NONBLOCK);
}

static void set_cloexec(int fd) {
    int f = fcntl(fd, F_GETFD, 0);
    if (f >= 0) fcntl(fd, F_SETFD, f | FD_CLOEXEC);
}

/* ------------------------------------------------------------------ rooms */

/* Free both SDP blobs and clear the slot so room_get can reuse it. */
static void room_release(struct room *r) {
    free(r->offer);  r->offer  = NULL; r->offer_n  = 0;
    free(r->answer); r->answer = NULL; r->answer_n = 0;
    r->code[0] = 0;
}

/* Find a room by code, optionally creating it in an empty slot. Touched is
 * bumped on every hit; expiry is handled separately by gc_rooms(). */
static struct room *room_get(const char *code, int create) {
    time_t now = monotonic_now();
    struct room *empty = NULL, *found = NULL;
    for (int i = 0; i < MAX_ROOMS; i++) {
        struct room *r = &rooms[i];
        if (r->code[0] == 0) { if (!empty) empty = r; continue; }
        if (strcmp(r->code, code) == 0) { found = r; break; }
    }
    if (!found && create && empty) {
        memset(empty, 0, sizeof(*empty));
        strncpy(empty->code, code, CODE_MAX);
        found = empty;
    }
    if (found) found->touched = now;
    return found;
}

/* Reclaim rooms that haven't been touched for ROOM_TTL_S. Called once per
 * GC_INTERVAL_S from the main loop, independent of request traffic. */
static void gc_rooms(void) {
    time_t now = monotonic_now();
    for (int i = 0; i < MAX_ROOMS; i++) {
        struct room *r = &rooms[i];
        if (r->code[0] && now - r->touched > ROOM_TTL_S) room_release(r);
    }
}

static int room_index(struct room *r) { return r ? (int)(r - rooms) : -1; }

/* ------------------------------------------------------------ connections */

static void conn_release(struct conn *c) {
    if (c->fd >= 0) close(c->fd);
    free(c->rbuf);
    free(c->wbuf);
    memset(c, 0, sizeof(*c));
    c->fd = -1;
    c->wait_room = -1;
}

static struct conn *conn_alloc(int fd) {
    for (int i = 0; i < MAX_CONNS; i++) {
        struct conn *c = &conns[i];
        if (c->state == C_FREE && c->fd <= 0) {
            memset(c, 0, sizeof(*c));
            c->fd = fd;
            c->state = C_READ;
            c->deadline = monotonic_now() + IDLE_TIMEOUT_S;
            c->wait_room = -1;
            return c;
        }
    }
    return NULL;
}

/* Stage a response on the connection (the body is copied into a freshly
 * allocated send buffer). Moves the conn to C_WRITE. */
static void respond(struct conn *c, int status, const char *ctype,
                    const void *body, size_t body_n) {
    char hdr[320];
    int hn = snprintf(hdr, sizeof(hdr),
        "HTTP/1.1 %d %s\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %zu\r\n"
        "Access-Control-Allow-Origin: *\r\n"
        "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
        "Access-Control-Allow-Headers: Content-Type\r\n"
        "Cache-Control: no-store\r\n"
        "Connection: close\r\n\r\n",
        status, status_text(status), ctype, body_n);

    char *buf = malloc(hn + body_n);
    if (!buf) { conn_release(c); return; }
    memcpy(buf, hdr, hn);
    if (body_n) memcpy(buf + hn, body, body_n);

    free(c->wbuf);
    c->wbuf = buf;
    c->wlen = hn + body_n;
    c->woff = 0;
    c->state = C_WRITE;
    c->deadline = monotonic_now() + IDLE_TIMEOUT_S;
    c->wait_room = -1;
}

/* Match-and-wake all C_WAIT connections that are parked on this room/slot.
 * If the answer was just delivered to at least one waiter, the handshake is
 * complete — drop the room. respond() has already copied `blob` into each
 * waker's write buffer by then, so freeing the room's answer is safe. If
 * nobody was parked, the blob stays so a later GET can still pick it up
 * (and that GET path releases the room itself). */
static void wake_waiters(int ri, char slot, const void *blob, size_t n) {
    int woke = 0;
    for (int i = 0; i < MAX_CONNS; i++) {
        struct conn *w = &conns[i];
        if (w->state == C_WAIT && w->wait_room == ri && w->wait_slot == slot) {
            respond(w, 200, "application/sdp", blob, n);
            woke = 1;
        }
    }
    if (slot == 'a' && woke) room_release(&rooms[ri]);
}

/* ------------------------------------------------------------- dispatch */

/* Called once headers + body are fully buffered. */
static void dispatch(struct conn *c) {
    if (strcmp(c->method, "OPTIONS") == 0) {
        respond(c, 204, "text/plain", "", 0);
        return;
    }

    /* Liveness probe — clients hit this to confirm the relay is reachable
     * before committing to the full handshake. Cheap: no allocation, no
     * room access. */
    if (strcmp(c->path, "/health") == 0) {
        if (strcmp(c->method, "GET") != 0 && strcmp(c->method, "HEAD") != 0) {
            respond(c, 405, "text/plain", "no", 2);
            return;
        }
        respond(c, 200, "text/plain", "ok\n", 3);
        return;
    }

    char code[CODE_MAX + 1] = {0};
    char slot[8] = {0};
    if (sscanf(c->path, "/room/%15[A-Za-z0-9_-]/%7[a-z]", code, slot) != 2) {
        respond(c, 404, "text/plain", "no", 2);
        return;
    }
    int is_offer  = !strcmp(slot, "offer");
    int is_answer = !strcmp(slot, "answer");
    if (!is_offer && !is_answer) {
        respond(c, 404, "text/plain", "no", 2);
        return;
    }
    char slot_ch = is_offer ? 'o' : 'a';

    if (!strcmp(c->method, "POST")) {
        struct room *r = room_get(code, 1);
        if (!r) { respond(c, 503, "text/plain", "full", 4); return; }

        char  **blob = is_offer ? &r->offer  : &r->answer;
        size_t *blen = is_offer ? &r->offer_n : &r->answer_n;

        /* malloc(0) is implementation-defined; round up to 1 byte so an
         * empty body POST doesn't masquerade as OOM. */
        size_t alloc_n = c->body_want ? c->body_want : 1;
        char *copy = malloc(alloc_n);
        if (!copy) { respond(c, 503, "text/plain", "oom", 3); return; }
        if (c->body_want) memcpy(copy, c->rbuf + c->body_off, c->body_want);

        free(*blob);
        *blob = copy;
        *blen = c->body_want;

        wake_waiters(room_index(r), slot_ch, *blob, *blen);
        respond(c, 204, "text/plain", "", 0);
        return;
    }

    if (!strcmp(c->method, "GET")) {
        struct room *r = room_get(code, 1);
        if (!r) { respond(c, 503, "text/plain", "full", 4); return; }

        char  *blob = is_offer ? r->offer  : r->answer;
        size_t blen = is_offer ? r->offer_n : r->answer_n;
        if (blob) {
            respond(c, 200, "application/sdp", blob, blen);
            /* Answer delivery completes the SDP exchange — peers go P2P now,
             * so the relay can drop the room. The offer slot stays in case
             * the joiner needs to refetch after a crash/retry. */
            if (is_answer) room_release(r);
            return;
        }
        /* Park the connection: no IO interest, just sit on the deadline. */
        c->state = C_WAIT;
        c->wait_room = room_index(r);
        c->wait_slot = slot_ch;
        c->deadline = monotonic_now() + LONGPOLL_S;
        return;
    }

    respond(c, 405, "text/plain", "no", 2);
}

/* ------------------------------------------------------------- IO drivers */

/* Parse "METHOD SP PATH SP HTTP/x.y" without mutating rbuf — the subsequent
 * Content-Length scan needs the header block intact. Returns 0 on success,
 * 400 on malformed, 414 on field overflow. */
static int parse_request_line(struct conn *c, const char *hdr_end) {
    const char *p = c->rbuf;
    const char *sp1 = memchr(p, ' ', (size_t)(hdr_end - p));
    if (!sp1) return 400;
    size_t mlen = (size_t)(sp1 - p);
    if (mlen > METHOD_MAX) return 414;
    memcpy(c->method, p, mlen);
    c->method[mlen] = 0;

    p = sp1 + 1;
    const char *sp2 = memchr(p, ' ', (size_t)(hdr_end - p));
    if (!sp2) return 400;
    size_t plen = (size_t)(sp2 - p);
    if (plen > PATH_MAX_LEN) return 414;
    memcpy(c->path, p, plen);
    c->path[plen] = 0;
    return 0;
}

static void on_readable(struct conn *c) {
    if (!c->rbuf) {
        c->rbuf = malloc(RBUF_SIZE);
        if (!c->rbuf) { conn_release(c); return; }
    }
    for (;;) {
        size_t cap = RBUF_SIZE - c->rlen;
        if (cap == 0) { conn_release(c); return; }
        ssize_t n = recv(c->fd, c->rbuf + c->rlen, cap, 0);
        if (n == 0) { conn_release(c); return; }
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) return;
            conn_release(c); return;
        }
        c->rlen += n;
        c->deadline = monotonic_now() + IDLE_TIMEOUT_S;

        /* Parse headers once \r\n\r\n appears within the first MAX_HEADER bytes. */
        if (c->body_off == 0) {
            size_t scan = c->rlen < MAX_HEADER ? c->rlen : MAX_HEADER;
            char *eoh = NULL;
            for (size_t i = 0; i + 3 < scan; i++) {
                if (c->rbuf[i] == '\r' && c->rbuf[i+1] == '\n' &&
                    c->rbuf[i+2] == '\r' && c->rbuf[i+3] == '\n') { eoh = c->rbuf + i; break; }
            }
            if (!eoh) {
                if (c->rlen >= MAX_HEADER) respond(c, 413, "text/plain", "header too large", 16);
                return;
            }
            *eoh = 0; /* terminate header block for ci_strstr */
            c->body_off = (size_t)(eoh - c->rbuf) + 4;

            int err = parse_request_line(c, eoh);
            if (err == 400) { respond(c, 400, "text/plain", "bad request", 11); return; }
            if (err == 414) { respond(c, 414, "text/plain", "uri too long", 12); return; }

            /* Parse Content-Length line-anchored to avoid matching header
             * names that merely *contain* "Content-Length:" as a substring
             * (e.g. X-Original-Content-Length). RFC 7230 §3.3.2 requires
             * rejecting duplicate Content-Length headers — important for
             * direct-bind deployments not fronted by a normalizing proxy. */
            int cl_seen = 0;
            const char *needle = "\nContent-Length:";
            const size_t needle_n = 16;
            for (char *p = c->rbuf; (p = ci_strstr(p, needle)) != NULL; p += needle_n) {
                if (cl_seen++) {
                    respond(c, 400, "text/plain", "duplicate content-length", 24);
                    return;
                }
                const char *v = p + needle_n;
                while (*v == ' ' || *v == '\t') v++;
                if (*v == '-' || *v == '+') {
                    respond(c, 400, "text/plain", "bad content-length", 18);
                    return;
                }
                char *end;
                errno = 0;
                unsigned long len = strtoul(v, &end, 10);
                if (end == v || errno == ERANGE) {
                    respond(c, 400, "text/plain", "bad content-length", 18);
                    return;
                }
                c->body_want = len;
            }
            if (c->body_want > MAX_BLOB) {
                respond(c, 413, "text/plain", "body too large", 14); return;
            }
        }

        if (c->rlen - c->body_off >= c->body_want) { dispatch(c); return; }
        /* else loop and read more */
    }
}

static void on_writable(struct conn *c) {
    while (c->woff < c->wlen) {
        /* SIGPIPE is suppressed via sigaction(SIGPIPE, SIG_IGN), so plain
         * send() is enough — no need for the non-portable MSG_NOSIGNAL. */
        ssize_t n = send(c->fd, c->wbuf + c->woff, c->wlen - c->woff, 0);
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) return;
            conn_release(c); return;
        }
        c->woff += n;
    }
    conn_release(c);
}

static void accept_new(void) {
    for (;;) {
        int cfd = accept(server_fd, NULL, NULL);
        if (cfd < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) return;
            return;
        }
        set_cloexec(cfd);
        if (set_nonblock(cfd) < 0 || !conn_alloc(cfd)) close(cfd); /* full or fcntl failed */
    }
}

/* ------------------------------------------------------------------ main */

int main(int argc, char **argv) {
    int port = 8080;
    if (argc > 1) {
        char *end;
        errno = 0;
        long lport = strtol(argv[1], &end, 10);
        if (errno || *end || lport < 1 || lport > 65535) {
            fprintf(stderr, "bad port: %s\n", argv[1]);
            return 1;
        }
        port = (int)lport;
    }

    /* sigaction() has portable, well-defined semantics across POSIX systems;
     * signal()'s behavior is historically split SysV/BSD. */
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = SIG_IGN;
    sigaction(SIGPIPE, &sa, NULL);
    sa.sa_handler = on_stop;
    sigaction(SIGINT,  &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);

    /* Raise RLIMIT_NOFILE up to what MAX_CONNS needs (plus a few for stdio /
     * the listen socket). If the hard cap is below that, warn — accepts will
     * silently fail at the kernel layer otherwise. */
    struct rlimit rl;
    if (getrlimit(RLIMIT_NOFILE, &rl) == 0) {
        rlim_t need = (rlim_t)MAX_CONNS + 16;
        if (rl.rlim_cur < need) {
            rl.rlim_cur = rl.rlim_max < need ? rl.rlim_max : need;
            setrlimit(RLIMIT_NOFILE, &rl);
            if (rl.rlim_cur < need) {
                fprintf(stderr, "warning: nofile soft limit %lu < %lu — capacity reduced\n",
                        (unsigned long)rl.rlim_cur, (unsigned long)need);
            }
        }
    }

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) { perror("socket"); return 1; }
    set_cloexec(server_fd);

    int one = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) {
        perror("setsockopt SO_REUSEADDR"); /* non-fatal */
    }

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons((uint16_t)port)
    };
    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) ||
        listen(server_fd, 64)) {
        perror("bind/listen"); return 1;
    }
    /* Flip the listener non-blocking only just before entering the accept
     * loop — bind/listen don't care, and this keeps the setup order
     * conventional. */
    if (set_nonblock(server_fd) < 0) { perror("fcntl"); return 1; }

    fprintf(stderr, "signaling server listening on :%d\n", port);

    for (int i = 0; i < MAX_CONNS; i++) { conns[i].fd = -1; conns[i].wait_room = -1; }

    time_t last_gc = monotonic_now();
    struct pollfd pfds[MAX_CONNS + 1];
    int           pidx[MAX_CONNS + 1];   /* maps pfd index → conns[] index */

    while (!stop_flag) {
        time_t now = monotonic_now();
        int nfd = 0;
        int timeout_ms = 1000;
        pfds[nfd++] = (struct pollfd){ .fd = server_fd, .events = POLLIN };

        /* Single sweep over the conn table: fire expired deadlines, build
         * the pfds array, and compute the poll timeout in one pass. */
        for (int i = 0; i < MAX_CONNS; i++) {
            struct conn *c = &conns[i];
            if (c->state == C_FREE || c->fd < 0) continue;

            if (c->deadline <= now) {
                /* C_WAIT → 204 No Content (not 408: Firefox auto-retries 408
                 * internally per RFC 7231 §6.5.7 and only surfaces a single
                 * fetch attempt to JS, eventually giving up with a
                 * CORS-adjacent NetworkError. 204 carries no retry semantics). */
                if (c->state == C_WAIT) {
                    respond(c, 204, "text/plain", "", 0);
                    /* respond() moved this conn to C_WRITE with a fresh
                     * deadline; fall through to register POLLOUT. */
                } else {
                    conn_release(c);
                    continue;
                }
            }

            short ev = 0;
            switch (c->state) {
            case C_READ:  ev = POLLIN;  break;
            case C_WRITE: ev = POLLOUT; break;
            /* WAIT: register POLLIN so a client disconnect (read-side EOF)
             * surfaces as a poll event; we never actually read while parked. */
            case C_WAIT:  ev = POLLIN;  break;
            default: continue;
            }
            pfds[nfd] = (struct pollfd){ .fd = c->fd, .events = ev };
            pidx[nfd] = i;
            nfd++;

            long dt = (long)(c->deadline - now) * 1000;
            if (dt < 0) dt = 0;
            if (dt < timeout_ms) timeout_ms = (int)dt;
        }

        int rc = poll(pfds, nfd, timeout_ms);
        if (rc < 0) {
            if (errno == EINTR) continue;
            perror("poll"); break;
        }

        if (pfds[0].revents & POLLIN) accept_new();

        for (int k = 1; k < nfd; k++) {
            short rev = pfds[k].revents;
            if (!rev) continue;
            struct conn *c = &conns[pidx[k]];
            /* The conn may have been released this iteration (e.g. via wake);
             * stale entries are no-ops since fd would be -1. */
            if (c->fd != pfds[k].fd) continue;

            if (c->state == C_WAIT) {
                /* We set Connection: close on every response, so a parked
                 * GET should send nothing further until the next request.
                 * Any POLLIN means either the client closed (recv == 0) or
                 * it pipelined / sent garbage — drop the conn either way. */
                if (rev & (POLLIN | POLLHUP | POLLERR | POLLNVAL)) conn_release(c);
                continue;
            }
            if (rev & (POLLERR | POLLNVAL)) { conn_release(c); continue; }
            if (c->state == C_READ  && (rev & POLLIN))  on_readable(c);
            else if (c->state == C_WRITE && (rev & POLLOUT)) on_writable(c);
            else if (rev & POLLHUP) conn_release(c);
        }

        /* Periodic room GC. The poll timeout is clamped to 1 s so this fires
         * on a wall-clock cadence even when the server is otherwise idle. */
        time_t tick = monotonic_now();
        if (tick - last_gc >= GC_INTERVAL_S) { gc_rooms(); last_gc = tick; }
    }

    fprintf(stderr, "signaling server shutting down\n");
    if (server_fd >= 0) close(server_fd);
    for (int i = 0; i < MAX_CONNS; i++) {
        if (conns[i].fd >= 0) conn_release(&conns[i]);
    }
    for (int i = 0; i < MAX_ROOMS; i++) {
        if (rooms[i].code[0]) room_release(&rooms[i]);
    }
    return 0;
}
