// sb-rec.c - SonoBus audio receiver
// Listens to a single source by nickname, decodes Opus, writes s16le to stdout
// All status messages go to stderr

#include "sb-send.h"

_Atomic int g_running = 1;

static char target_nick[64] = {0};
static bool raw_opus = false;
static bool ogg_output = false;
static int record_seconds = 0;
static OpusMSDecoder *decoder = NULL;
static bool dec_joint_stereo = false;
static int32_t our_sink_id = 0;

static OggWriter g_ogg;
static float g_pcm[OPUS_MAX_SAMPLES * 2];
static int16_t g_s16[OPUS_MAX_SAMPLES * 2];

static void rec_signal_handler(int sig) {
    (void)sig;
    atomic_store(&g_running, 0);
    const char msg[] = "\nsb-rec: signal received, shutting down...\n";
    (void)write(STDERR_FILENO, msg, sizeof(msg) - 1);
}

static bool is_target_peer(Peer *p) {
    return p && strcmp(p->name, target_nick) == 0;
}

static void uninvite_peer(AppState *state, Peer *p) {
    unsigned char buf[128];
    int len = build_uninvite_msg(0, p->our_source_id, buf);
    sendto(state->udp_sock, (const char*)buf, len, 0,
           (struct sockaddr *)&p->addr, sizeof(p->addr));
}

static void handle_udp_rec(AppState *state, unsigned char *buf, int len, struct sockaddr_in *from) {
    SB_ASSERT(state->udp_sock != SOCK_INVALID);
    char addr[128];
    int pos = osc_read_string(buf, addr, sizeof(addr));
    SB_ASSERT(pos > 0 && pos <= len);
    (void)len;

    // Invite: only accept from target peer, uninvite everyone else
    if (strncmp(addr, "/aoo/src/", 9) == 0) {
        char *slash = strchr(addr + 9, '/');
        if (slash && strcmp(slash, "/invite") == 0) {
            char tags[16];
            pos += osc_read_string(buf + pos, tags, sizeof(tags));
            int32_t their_sink_id = 0;
            if (strlen(tags) >= 2 && tags[1] == 'i') {
                their_sink_id = osc_read_int32(buf + pos);
            }

            pthread_mutex_lock(&state->peer_mutex);
            Peer *p = find_peer_by_addr_locked(state, from);
            if (p && is_target_peer(p)) {
                bool was_connected = p->connected;
                bool sink_changed = (p->remote_sink_id != their_sink_id);
                p->remote_sink_id = their_sink_id;
                p->our_source_id = their_sink_id;
                p->connected = true;
                bool should_send_format = !was_connected || sink_changed;
                if (should_send_format) {
                    p->salt = generate_salt(p);
                    p->salt_initialized = true;
                    atomic_store(&p->sequence, 0);
                    p->format_sent = true;
                    fprintf(stderr, "sb-rec: target %s connected (sink=%d salt=%d)\n",
                            p->name, p->remote_sink_id, p->salt);
                }
                Peer pcopy = *p;
                pthread_mutex_unlock(&state->peer_mutex);
                if (should_send_format) {
                    send_format_to_peer(state, &pcopy);
                }
            } else if (p) {
                Peer pcopy = *p;
                pthread_mutex_unlock(&state->peer_mutex);
                fprintf(stderr, "sb-rec: uninviting non-target peer %s\n", pcopy.name);
                uninvite_peer(state, &pcopy);
            } else {
                pthread_mutex_unlock(&state->peer_mutex);
            }
        }
        else if (slash && strcmp(slash, "/uninvite") == 0) {
            return;
        }
    }
    // Data message: decode and write to stdout (only from target)
    else if (strncmp(addr, "/aoo/sink/", 10) == 0 && strstr(addr + 10, "/data")) {
        int32_t addr_sink = atoi(addr + 10);

        pthread_mutex_lock(&state->peer_mutex);
        Peer *p = find_peer_by_addr_locked(state, from);
        bool target = is_target_peer(p);
        if (p) { p->last_packet_time = time(NULL); }
        pthread_mutex_unlock(&state->peer_mutex);
        if (!target) {
            pthread_mutex_lock(&state->peer_mutex);
            Peer *pn = find_peer_by_addr_locked(state, from);
            if (pn) {
                Peer pcopy = *pn;
                pthread_mutex_unlock(&state->peer_mutex);
                fprintf(stderr, "sb-rec: ignoring data from non-target %s, uninviting\n", pcopy.name);
                uninvite_peer(state, &pcopy);
            } else {
                pthread_mutex_unlock(&state->peer_mutex);
            }
            return;
        }

        char tags[32];
        pos += osc_read_string(buf + pos, tags, sizeof(tags));
        pos += 4; // source_id
        int32_t pkt_salt = osc_read_int32(buf + pos); pos += 4;
        pos += 4; // seq
        pos += 8; // samplerate double
        pos += 4; // skipped
        pos += 4; // data_len
        pos += 4; // flags
        pos += 4; // reserved
        int32_t blob_len = osc_read_int32(buf + pos); pos += 4;
        unsigned char *opus_data = buf + pos;

        if (addr_sink != our_sink_id) {
            fprintf(stderr, "sb-rec: data sink %d != expected %d, accepting from target\n",
                    addr_sink, our_sink_id);
            our_sink_id = addr_sink;
        }
        if (blob_len < 0) { return; }
        if (blob_len > 0 && (int64_t)pos + blob_len > len) {
            fprintf(stderr, "sb-rec: data blob truncated (blob_len=%d, remaining=%d)\n",
                    blob_len, len - pos + 4);
            return;
        }

        if (raw_opus) {
            uint32_t pkt_len = (uint32_t)blob_len;
            ssize_t written = 0;
            ssize_t want = 4 + pkt_len;
            unsigned char pkt_header[4];
            pkt_header[0] = pkt_len; pkt_header[1] = pkt_len >> 8;
            pkt_header[2] = pkt_len >> 16; pkt_header[3] = pkt_len >> 24;
            while (written < 4) {
                ssize_t n = write(STDOUT_FILENO, (char *)pkt_header + written, 4 - written);
                if (n <= 0) { atomic_store(&g_running, 0); return; }
                written += n;
            }
            written = 0;
            while (written < pkt_len) {
                ssize_t n = write(STDOUT_FILENO, (char *)opus_data + written, pkt_len - written);
                if (n <= 0) { atomic_store(&g_running, 0); return; }
                written += n;
            }
            return;
        }

        if (ogg_output) {
            if (!g_ogg.started) {
                ogg_writer_init(&g_ogg, state->channels, "sb-rec " VERSION, stdout);
                 fprintf(stderr, "sb-rec: Ogg/Opus output initialized (%d ch)\n",
                         g_ogg.channels);
            }
            ogg_writer_packet(&g_ogg, opus_data, blob_len, stdout);
            return;
        }

        if (!decoder) { return; }

        int frames = opus_decode_to_s16(decoder, opus_data, blob_len, g_s16, g_pcm, state->channels);
        if (frames <= 0) {
            fprintf(stderr, "sb-rec: opus decode failed (err=%d, len=%d)\n",
                    frames, blob_len);
            return;
        }

        ssize_t written = 0;
        ssize_t want = frames * state->channels * sizeof(int16_t);
        while (written < want) {
            ssize_t n = write(STDOUT_FILENO, (char *)g_s16 + written, want - written);
            if (n <= 0) { atomic_store(&g_running, 0); return; }
            written += n;
        }
    }
    // Format message: initialize decoder (only from target)
    else if (strncmp(addr, "/aoo/sink/", 10) == 0 && strstr(addr + 10, "/format")) {
        int32_t addr_sink = atoi(addr + 10);

        pthread_mutex_lock(&state->peer_mutex);
        Peer *p = find_peer_by_addr_locked(state, from);
        bool target = is_target_peer(p);
        pthread_mutex_unlock(&state->peer_mutex);
        if (!target) { return; }

        if (addr_sink != our_sink_id) {
            fprintf(stderr, "sb-rec: format sink %d != expected %d, accepting from target\n",
                    addr_sink, our_sink_id);
            our_sink_id = addr_sink;
        }

        char tags[32];
        pos += osc_read_string(buf + pos, tags, sizeof(tags));
        pos += 4; // source_id
        pos += 4; // version
        int32_t salt = osc_read_int32(buf + pos); pos += 4;
        int32_t nch = osc_read_int32(buf + pos); pos += 4;
        if (nch < 1 || nch > 2) {
            fprintf(stderr, "sb-rec: unsupported channel count %d, must be 1-2\n", nch);
            return;
        }
        int32_t rate = osc_read_int32(buf + pos); pos += 4;
        int32_t blocksize = osc_read_int32(buf + pos); pos += 4;
        char codec[16];
        pos += osc_read_string(buf + pos, codec, sizeof(codec));
        pos += 4; // opus_opts length

        if (rate != SAMPLE_RATE) {
            fprintf(stderr, "sb-rec: unsupported sample rate %d, must be %d\n", rate, SAMPLE_RATE);
            return;
        }
        // Valid Opus frame sizes at 48kHz: 2.5, 5, 10, 20, 40, 60 ms
        if (blocksize != 120 && blocksize != 240 && blocksize != 480 &&
            blocksize != 960 && blocksize != 1920 && blocksize != 2880) {
            fprintf(stderr, "sb-rec: invalid blocksize %d, must be 120/240/480/960/1920/2880\n", blocksize);
            return;
        }
        state->frame_size = blocksize;
        if (strcmp(codec, "opus") != 0) {
            fprintf(stderr, "sb-rec: unsupported codec '%s'\n", codec);
            return;
        }
        int streams, coupled_streams;
        unsigned char mapping[255];
        compute_opus_mapping(state->channels, dec_joint_stereo, &streams, &coupled_streams, mapping);

        if (decoder) { opus_multistream_decoder_destroy(decoder); decoder = NULL; }
        if (init_opus_decoder_ex(&decoder, rate, state->channels, streams, coupled_streams, mapping) < 0) {
            return;
        }
        fprintf(stderr, "sb-rec: decoder ready %d Hz %d ch (salt=%d)\n",
                rate, state->channels, salt);
}
    // Source ping from sender: respond so sender knows we're alive
    else if (strncmp(addr, "/aoo/sink/", 10) == 0 && strstr(addr + 10, "/ping")) {
        pthread_mutex_lock(&state->peer_mutex);
        Peer *p = find_peer_by_addr_locked(state, from);
        if (p && is_target_peer(p)) {
            time_t now = time(NULL);
            if (now - p->last_ping_reply >= 1) {
                p->last_ping_reply = now;
                pthread_mutex_unlock(&state->peer_mutex);
                unsigned char ping_buf[64];
                int plen = build_peer_ping_msg(state->client_token, ping_buf);
                sendto(state->udp_sock, (const char*)ping_buf, plen, 0,
                       (struct sockaddr *)from, sizeof(*from));
            } else {
                pthread_mutex_unlock(&state->peer_mutex);
            }
        } else {
            pthread_mutex_unlock(&state->peer_mutex);
        }
    }
    // Peer ping: only reply to target peer
    else if (strcmp(addr, "/aoo/peer/ping") == 0) {
        pthread_mutex_lock(&state->peer_mutex);
        Peer *p = find_peer_by_addr_locked(state, from);
        if (p && is_target_peer(p)) {
            time_t now = time(NULL);
            if (now - p->last_ping_reply >= 1) {
                p->last_ping_reply = now;
                pthread_mutex_unlock(&state->peer_mutex);
                unsigned char ping_buf[64];
                int plen = build_peer_ping_msg(state->client_token, ping_buf);
                sendto(state->udp_sock, (const char*)ping_buf, plen, 0,
                       (struct sockaddr *)from, sizeof(*from));
            } else {
                pthread_mutex_unlock(&state->peer_mutex);
            }
        } else {
            pthread_mutex_unlock(&state->peer_mutex);
            dbg(state, "sb-rec: ignoring ping from non-target\n");
        }
    }
}

static void process_tcp(AppState *state, unsigned char *buf, int len) {
    SB_ASSERT(state->tcp_sock != SOCK_INVALID);
    char addr[128], tags[32];
    int pos = osc_read_string(buf, addr, sizeof(addr));
    SB_ASSERT(pos > 0 && pos <= len);
    (void)len;

    fprintf(stderr, "sb-rec: tcp %s\n", addr);

    if (strcmp(addr, "/aoo/client/login") == 0) {
        pos += osc_read_string(buf + pos, tags, sizeof(tags));
        int32_t status = osc_read_int32(buf + pos);
        if (status > 0) {
            atomic_store(&state->logged_in, true);
            fprintf(stderr, "sb-rec: login successful\n");
        } else {
            fprintf(stderr, "sb-rec: login failed\n");
            atomic_store(&state->running, false);
        }
    }
    else if (strcmp(addr, "/aoo/client/group/join") == 0) {
        pos += osc_read_string(buf + pos, tags, sizeof(tags));
        char grp[64];
        pos += osc_read_string(buf + pos, grp, sizeof(grp));
        int32_t status = osc_read_int32(buf + pos);
        if (status > 0) {
            atomic_store(&state->group_joined, true);
            fprintf(stderr, "sb-rec: joined group %s\n", grp);
        } else {
            fprintf(stderr, "sb-rec: failed to join group\n");
            atomic_store(&state->running, false);
        }
    }
    else if (strcmp(addr, "/aoo/client/peer/join") == 0) {
        char name[64], ip[64];
        int port;
        int64_t token;
        if (parse_peer_join(buf, len, name, ip, &port, &token) == 0) {
            fprintf(stderr, "sb-rec: peer joined: %s (%s:%d)\n", name, ip, port);
            pthread_mutex_lock(&state->peer_mutex);
            Peer *p = add_peer_locked(state, name, ip, port, token);
            Peer pcopy;
            bool need_invite = false;
            if (p && strcmp(name, target_nick) == 0) {
                pcopy = *p;
                our_sink_id = p->our_source_id;
                p->last_invite_time = time(NULL);
                p->invite_count++;
                need_invite = true;
                fprintf(stderr, "sb-rec: target source '%s' found (sink_id=%d), sending invite\n", target_nick, our_sink_id);
            }
            pthread_mutex_unlock(&state->peer_mutex);
            if (need_invite) { send_invite_to_peer(state, &pcopy); }
        }
    }
    else if (strcmp(addr, "/aoo/client/peer/leave") == 0) {
        char name[64];
        if (parse_peer_leave(buf, len, name) == 0) {
            fprintf(stderr, "sb-rec: peer left: %s\n", name);
            pthread_mutex_lock(&state->peer_mutex);
            remove_peer_locked(state, name);
            pthread_mutex_unlock(&state->peer_mutex);
            if (strcmp(name, target_nick) == 0) {
                fprintf(stderr, "sb-rec: target source '%s' left, waiting for rejoin\n", target_nick);
            }
        }
    }
}

static int connect_and_login(AppState *state) {
    struct addrinfo hints = {0}, *res = NULL;
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    char port_str[16];
    snprintf(port_str, sizeof(port_str), "%d", state->server_port);

    if (getaddrinfo(state->server, port_str, &hints, &res) != 0 || !res) {
        fprintf(stderr, "sb-rec: cannot resolve: %s\n", state->server);
        return -1;
    }

    struct sockaddr_in srv_addr;
    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(state->server_port);
    memcpy(&srv_addr.sin_addr, &((struct sockaddr_in*)res->ai_addr)->sin_addr, sizeof(struct in_addr));
    freeaddrinfo(res);

    state->udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (state->udp_sock == SOCK_INVALID) { fprintf(stderr, "sb-rec: UDP socket failed: %d\n", SOCK_ERR()); return -1; }
#ifdef _WIN32
    u_long nonblocking = 1;
    ioctlsocket(state->udp_sock, FIONBIO, &nonblocking);
#endif

    struct sockaddr_in local = {0};
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = INADDR_ANY;
    if (bind(state->udp_sock, (struct sockaddr *)&local, sizeof(local)) < 0) {
        fprintf(stderr, "sb-rec: UDP bind failed: %d\n", SOCK_ERR()); return -1;
    }

    socklen_t alen = sizeof(local);
    getsockname(state->udp_sock, (struct sockaddr *)&local, &alen);
    state->local_port = ntohs(local.sin_port);

    fprintf(stderr, "sb-rec: discovering public IP...\n");
    if (discover_public_ip(state, &srv_addr) < 0) {
        fprintf(stderr, "sb-rec: failed to discover public IP, cannot continue\n");
        CLOSE_SOCKET(state->udp_sock);
        return -1;
    }

    state->tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (state->tcp_sock == SOCK_INVALID) { fprintf(stderr, "sb-rec: TCP socket failed: %d\n", SOCK_ERR()); return -1; }

    fprintf(stderr, "sb-rec: connecting to %s:%d\n", state->server, state->server_port);
    if (connect(state->tcp_sock, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
        fprintf(stderr, "sb-rec: connect failed: %d\n", SOCK_ERR()); return -1;
    }

    {
        int enable = 1;
        setsockopt(state->tcp_sock, SOL_SOCKET, SO_KEEPALIVE, (const char*)&enable, sizeof(enable));
#ifdef __linux__
        int idle = 30;
        setsockopt(state->tcp_sock, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
        int interval = 10;
        setsockopt(state->tcp_sock, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
        int count = 3;
        setsockopt(state->tcp_sock, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));
#elif defined(_WIN32)
        DWORD bytes = 0;
        struct { u_long onoff; u_long keepalivetime; u_long keepaliveinterval; } ka = {1, 30000, 10000};
        WSAIoctl(state->tcp_sock, SIO_KEEPALIVE_VALS, &ka, sizeof(ka), NULL, 0, &bytes, NULL, NULL);
#endif
    }

    fprintf(stderr, "sb-rec: connected\n");

    unsigned char buf[1024];
    int len = build_login_msg(state, buf);
    if (send_tcp_slip(state->tcp_sock, buf, len) < 0) { fprintf(stderr, "sb-rec: login send failed\n"); return -1; }

    slip_decoder_init(&state->slip);
    unsigned char recv_buf[1024];

    for (int attempt = 0; attempt < 50 && atomic_load(&g_running); attempt++) {
        fd_set fds;
        struct timeval tv = {0, 100000};
        FD_ZERO(&fds);
        FD_SET(state->tcp_sock, &fds);
        if (select(state->tcp_sock + 1, &fds, NULL, NULL, &tv) > 0) {
            int n = recv(state->tcp_sock, (char*)recv_buf, sizeof(recv_buf), 0);
            if (n <= 0) { fprintf(stderr, "sb-rec: server closed connection\n"); return -1; }
            for (int i = 0; i < n; i++) {
                unsigned char *msg;
                int msglen;
                if (slip_decode_byte(&state->slip, recv_buf[i], &msg, &msglen)) {
                    char maddr[64];
                    osc_read_string(msg, maddr, sizeof(maddr));
                    if (strcmp(maddr, "/aoo/client/login") == 0) {
                        process_tcp(state, msg, msglen);
                        if (atomic_load(&state->logged_in)) { goto login_done; }
                        if (!atomic_load(&state->running)) { return -1; }
                    }
                }
            }
        }
    }
    fprintf(stderr, "sb-rec: login timeout\n");
    return -1;

login_done:
    len = build_group_join_msg(state, buf);
    if (send_tcp_slip(state->tcp_sock, buf, len) < 0) { fprintf(stderr, "sb-rec: group join send failed\n"); return -1; }

    fprintf(stderr, "sb-rec: joining group %s\n", state->group);
    slip_decoder_init(&state->slip);

    for (int attempt = 0; attempt < 100 && atomic_load(&g_running); attempt++) {
        fd_set fds;
        struct timeval tv = {0, 100000};
        FD_ZERO(&fds);
        FD_SET(state->tcp_sock, &fds);
        if (select(state->tcp_sock + 1, &fds, NULL, NULL, &tv) > 0) {
            int n = recv(state->tcp_sock, (char*)recv_buf, sizeof(recv_buf), 0);
            if (n <= 0) { fprintf(stderr, "sb-rec: server closed connection\n"); return -1; }
            for (int i = 0; i < n; i++) {
                unsigned char *msg;
                int msglen;
                if (slip_decode_byte(&state->slip, recv_buf[i], &msg, &msglen)) {
                    process_tcp(state, msg, msglen);
                    if (atomic_load(&state->group_joined)) { goto group_done; }
                    if (!atomic_load(&state->running)) { return -1; }
                }
            }
        }
    }
    fprintf(stderr, "sb-rec: group join timeout\n");
    return -1;

group_done:
    return 0;
}

static void *server_thread(void *arg) {
    AppState *state = (AppState *)arg;
    SB_ASSERT(state->tcp_sock != SOCK_INVALID);
    slip_decoder_init(&state->slip);
    unsigned char recv_buf[4096];
    time_t last_maint = 0;

    while (g_running && atomic_load(&state->running)) {
        fd_set fds;
        struct timeval tv = {0, 500000};
        FD_ZERO(&fds);
        FD_SET(state->tcp_sock, &fds);

        int ret = select(state->tcp_sock + 1, &fds, NULL, NULL, &tv);
        if (ret < 0) { if (SOCK_ERR() == EINTR) { continue; } break; }

        if (ret > 0 && FD_ISSET(state->tcp_sock, &fds)) {
            int n = recv(state->tcp_sock, (char*)recv_buf, sizeof(recv_buf), 0);
            if (n <= 0) {
                fprintf(stderr, "sb-rec: server disconnected\n");
                atomic_store(&state->running, false);
                break;
            }
            for (int i = 0; i < n; i++) {
                unsigned char *msg;
                int msglen;
                if (slip_decode_byte(&state->slip, recv_buf[i], &msg, &msglen)) {
                    process_tcp(state, msg, msglen);
                }
            }
        }

        time_t now = time(NULL);
        if (now - last_maint >= MAINTENANCE_INTERVAL) {
            last_maint = now;
            {
                unsigned char probe = SLIP_END;
                int n = send(state->tcp_sock, (const char*)&probe, 1, 0);
                if (n <= 0) {
                    fprintf(stderr, "sb-rec: server connection lost\n");
                    atomic_store(&state->running, false);
                    break;
                }
            }
            pthread_mutex_lock(&state->peer_mutex);
            int count = state->peers.count;
            for (int i = 0; i < count; i++) {
                Peer *p = &state->peers.items[i];
                if (!p->active || !is_target_peer(p)) { continue; }
                if (p->active && p->remote_sink_id != 0 && p->salt_initialized) {
                    Peer pcopy = *p;
                    pthread_mutex_unlock(&state->peer_mutex);
                    send_source_ping(state, &pcopy);
                    unsigned char ping_buf[64];
                    int plen = build_peer_ping_msg(state->client_token, ping_buf);
                    sendto(state->udp_sock, (const char*)ping_buf, plen, 0,
                           (struct sockaddr *)&pcopy.addr, sizeof(pcopy.addr));
                    dbg(state, "sb-rec: sent ping to %s\n", pcopy.name);
                    pthread_mutex_lock(&state->peer_mutex);
                    if (i < state->peers.count && state->peers.items[i].active) {
                        p = &state->peers.items[i];
                    } else {
                        continue;
                    }
                }
                if (now - p->last_invite_time > 2) {
                    bool stale = (p->last_packet_time == 0) ||
                                 (now - p->last_packet_time > 30);
                    if (stale) {
                        fprintf(stderr, "sb-rec: no data from target, retrying invite\n");
                        p->connected = false;
                        p->format_sent = false;
                        p->salt_initialized = false;
                        p->last_invite_time = now;
                        p->last_packet_time = 0;
                        p->our_source_id = ++state->next_sink_id;
                        our_sink_id = p->our_source_id;
                        Peer pcopy = *p;
                        pthread_mutex_unlock(&state->peer_mutex);
                        send_invite_to_peer(state, &pcopy);
                        pthread_mutex_lock(&state->peer_mutex);
                        if (i < state->peers.count && state->peers.items[i].active) {
                            p = &state->peers.items[i];
                        } else {
                            continue;
                        }
                    }
                }
            }
            peer_list_compact(&state->peers);
            pthread_mutex_unlock(&state->peer_mutex);
        }
    }
    return NULL;
}

static void *udp_thread(void *arg) {
    AppState *state = (AppState *)arg;
    unsigned char buf[MAX_PACKET_SIZE];

    while (g_running && atomic_load(&state->running)) {
        fd_set fds;
        struct timeval tv = {0, 500000};
        FD_ZERO(&fds);
        FD_SET(state->udp_sock, &fds);

        int ret = select(state->udp_sock + 1, &fds, NULL, NULL, &tv);
        if (ret < 0) {
            int err = SOCK_ERR();
            if (err == EINTR) { continue; }
            break;
        }

        if (ret <= 0) {
            SLEEP_MS(1);
            continue;
        }

        if (FD_ISSET(state->udp_sock, &fds)) {
            int packets = 0;
            while (packets < 10) {
                struct sockaddr_in from;
                socklen_t fromlen = sizeof(from);
                int n = recvfrom(state->udp_sock, (char*)buf, sizeof(buf),
#ifdef _WIN32
                                 0,
#else
                                 MSG_DONTWAIT,
#endif
                                 (struct sockaddr *)&from, &fromlen);
                if (n <= 0) { break; }
                handle_udp_rec(state, buf, n, &from);
                packets++;
            }
        }
    }
    return NULL;
}

static void print_usage(const char *prog) {
    fprintf(stderr, "Usage: %s [options]\n", prog);
    fprintf(stderr, "  -s server  Connection server [%s]\n", DEFAULT_SERVER);
    fprintf(stderr, "  -p port    Server port [%d]\n", DEFAULT_PORT);
    fprintf(stderr, "  -g group   Group name [%s]\n", DEFAULT_GROUP);
    fprintf(stderr, "  -n nick    Our nickname [%s]\n", DEFAULT_NICK);
    fprintf(stderr, "  -N source  Source nickname to receive from (required)\n");
    fprintf(stderr, "  -c chans   Audio channels: 1 or 2 [%d]\n", DEFAULT_CHANNELS);
    fprintf(stderr, "  -J         Joint stereo (coupled streams, must match sender)\n");
    fprintf(stderr, "  -r         Raw Opus output to stdout (no decode)\n");
    fprintf(stderr, "  -o         Ogg/Opus output to stdout\n");
    fprintf(stderr, "  -t sec     Time limit: exit after sec seconds\n");
    fprintf(stderr, "  -h         Show help\n");
    fprintf(stderr, "  --debug    Enable debug output\n");
    fprintf(stderr, "  --version  Show version\n");
    fprintf(stderr, "\nDefault: raw s16le %d Hz audio to stdout\n", SAMPLE_RATE);
    fprintf(stderr, "With -r: length-prefixed raw Opus packets to stdout\n");
    fprintf(stderr, "With -o: Ogg/Opus container to stdout (playable)\n");
}

int main(int argc, char **argv) {
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--version") == 0) {
            fprintf(stderr, "sb-rec version %s\n", VERSION);
            fprintf(stderr, "libopus: %s\n", opus_get_version_string());
            fprintf(stderr, "Compiled %s at %s\n", COMPILE_DATE, COMPILE_TIME);
            return 0;
        }
    }

#ifndef _WIN32
    struct sigaction sa = { .sa_handler = rec_signal_handler };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    signal(SIGPIPE, SIG_IGN);
#endif

#ifdef _WIN32
    _setmode(_fileno(stdout), _O_BINARY);
#endif

    AppState state;
    memset(&state, 0, sizeof(state));
    strncpy(state.server, DEFAULT_SERVER, sizeof(state.server) - 1);
    state.server_port = DEFAULT_PORT;
    strncpy(state.group, DEFAULT_GROUP, sizeof(state.group) - 1);
    strncpy(state.nickname, DEFAULT_NICK, sizeof(state.nickname) - 1);
    state.channels = DEFAULT_CHANNELS;
    state.bitrate = DEFAULT_BITRATE;
    state.tcp_sock = SOCK_INVALID;
    state.udp_sock = SOCK_INVALID;
    memcpy(state.password_hash, MD5_EMPTY, 33);

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--debug") == 0) {
            state.debug = true;
            argv[i] = argv[0];
        }
    }

    opterr = 0;
    int opt;
    while ((opt = getopt(argc, argv, ":s:p:g:n:N:c:Jrt:oh")) != -1) {
        switch (opt) {
            case 's': strncpy(state.server, optarg, sizeof(state.server) - 1); break;
            case 'p': state.server_port = atoi(optarg); break;
            case 'g': strncpy(state.group, optarg, sizeof(state.group) - 1); break;
            case 'n': strncpy(state.nickname, optarg, sizeof(state.nickname) - 1); break;
            case 'N': strncpy(target_nick, optarg, sizeof(target_nick) - 1); break;
            case 'c':
                state.channels = atoi(optarg);
                if (state.channels != 1 && state.channels != 2) {
                    fprintf(stderr, "sb-rec: channels must be 1 or 2\n");
                    exit(1);
                }
                break;
            case 'J': dec_joint_stereo = true; break;
            case 'r': raw_opus = true; break;
            case 'o': ogg_output = true; break;
            case 't': record_seconds = atoi(optarg); break;
            case 'h':
                print_usage(argv[0]);
                exit(0);
            case ':':
                fprintf(stderr, "sb-rec: option '-%c' requires an argument\n", optopt);
                exit(1);
            case '?':
                if (optopt != 0) {
                    fprintf(stderr, "sb-rec: unknown option '-%c'\n", optopt);
                }
                exit(1);
            default:
                break;
        }
    }

    if (target_nick[0] == '\0') {
        fprintf(stderr, "sb-rec: -N source nickname is required\n");
        print_usage(argv[0]);
        exit(1);
    }

    if (raw_opus && ogg_output) {
        fprintf(stderr, "sb-rec: -r and -o are mutually exclusive\n");
        exit(1);
    }

    if (record_seconds < 0) {
        fprintf(stderr, "sb-rec: -t requires a positive integer (seconds)\n");
        exit(1);
    }

    fprintf(stderr, "sb-rec %s (receiving from '%s', %d ch)\n", VERSION, target_nick, state.channels);

    // Real SonoBus token - NO bit 63
    state.client_token = ((int64_t)time(NULL) << 32) | (unsigned)getpid();

    pthread_mutex_init(&state.peer_mutex, NULL);
    peer_list_init(&state.peers);
    atomic_store(&state.running, true);
    atomic_store(&state.logged_in, false);
    atomic_store(&state.group_joined, false);
    state.next_sink_id = 100;
    state.frame_size = 960;
    our_sink_id = 0;
    srand(time(NULL) ^ (unsigned)getpid());

    if (connect_and_login(&state) < 0) {
        peer_list_destroy(&state.peers);
        pthread_mutex_destroy(&state.peer_mutex);
        return 1;
    }

    fprintf(stderr, "sb-rec: listening (sink_id=%d)\n", our_sink_id);

    time_t start_time = time(NULL);

    pthread_t server_tid, udp_tid;
    pthread_create(&server_tid, NULL, server_thread, &state);
    pthread_create(&udp_tid, NULL, udp_thread, &state);

    while (g_running && atomic_load(&state.running)) {
        SLEEP_MS(1000);
        if (record_seconds > 0 && time(NULL) - start_time >= record_seconds) {
            fprintf(stderr, "sb-rec: time limit reached (%ds), exiting\n", record_seconds);
            atomic_store(&state.running, false);
        }
    }

    atomic_store(&state.running, false);
    pthread_join(udp_tid, NULL);
    pthread_join(server_tid, NULL);

    ogg_writer_finish(&g_ogg, stdout);

    if (state.tcp_sock != SOCK_INVALID) {
        unsigned char buf[256];
        int len = build_group_leave_msg(&state, buf);
        send_tcp_slip(state.tcp_sock, buf, len);
        CLOSE_SOCKET(state.tcp_sock);
    }
    if (state.udp_sock != SOCK_INVALID) {
        CLOSE_SOCKET(state.udp_sock);
    }
    if (decoder) { opus_multistream_decoder_destroy(decoder); }
    pthread_mutex_destroy(&state.peer_mutex);
    peer_list_destroy(&state.peers);
    return 0;
}
// EOF
