// sb-send: Sonobus audio sender
// Connects to SonoBus server and sends audio to group members.
// Build with -DALSA for ALSA capture on Linux, -DWASAPI for WASAPI on Windows.
// Without either: stdin (s16le 48k).

// Contains main program flow, parse arguments, start threads.

#include "sb-send.h"

void handle_udp_message(AppState *state, unsigned char *buf, int len, struct sockaddr_in *from);
void process_tcp_message(AppState *state, unsigned char *buf, int len);

static int connect_and_login(AppState *state);
static void *server_thread(void *arg);
static void *udp_thread(void *arg);
static void print_usage(const char *prog);

_Atomic int g_running = 1;

#ifdef _WIN32
BOOL WINAPI console_handler(DWORD ctrl_type) {
    if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT ||
        ctrl_type == CTRL_CLOSE_EVENT) {
        atomic_store(&g_running, 0);
        fprintf(stderr, "\nSignal received, shutting down...\n");
        return TRUE;
    }
    return FALSE;
}
#else
void signal_handler(int sig) {
    if (sig == SIGINT || sig == SIGTERM) {
        atomic_store(&g_running, 0);
        const char msg[] = "\nSignal received, shutting down...\n";
        (void)write(STDERR_FILENO, msg, sizeof(msg) - 1);
    }
}
#endif

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, "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, "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, "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, "UDP port: %d\n", state->local_port);

    fprintf(stderr, "Discovering public IP...\n");
    if (discover_public_ip(state, &srv_addr) < 0) {
        fprintf(stderr, "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, "TCP socket failed: %d\n", SOCK_ERR()); return -1; }

    fprintf(stderr, "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, "Connect failed: %d\n", SOCK_ERR()); return -1;
    }
    fprintf(stderr, "Connected\n");

    {
        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
    }

    unsigned char buf[1024];
    int len = build_login_msg(state, buf);
    if (send_tcp_slip(state->tcp_sock, buf, len) < 0) { fprintf(stderr, "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, "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 addr[64];
                    osc_read_string(msg, addr, sizeof(addr));
                    if (strcmp(addr, "/aoo/client/login") == 0) {
                        process_tcp_message(state, msg, msglen);
                        if (atomic_load(&state->logged_in)) { goto login_done; }
                        if (!atomic_load(&state->running)) { return -1; }
                    }
                }
            }
        }
    }
    fprintf(stderr, "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, "Group join send failed\n"); return -1; }

    fprintf(stderr, "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, "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_message(state, msg, msglen);
                    if (atomic_load(&state->group_joined)) { goto group_done; }
                    if (!atomic_load(&state->running)) { return -1; }
                }
            }
        }
    }
    fprintf(stderr, "Group join timeout\n");
    return -1;

group_done:
    return 0;
}

static void run_maintenance(AppState *state) {
    time_t now = time(NULL);
    pthread_mutex_lock(&state->peer_mutex);
    int count = state->peers.count;
    int connected = 0, pending = 0;
    for (int i = 0; i < count; i++) {
        Peer *p = &state->peers.items[i];
        if (!p->active || p->is_sb_send) { continue; }
        if (p->connected) { connected++; }
        else { pending++; }
    }
    dbg(state, "maintenance: %d peers (%d connected, %d pending)\n",
        count, connected, pending);

    for (int i = 0; i < state->peers.count; i++) {
        Peer *p = &state->peers.items[i];
        if (!p->active || p->is_sb_send) { continue; }
        if (!p->connected) {
            if (p->invite_count < INVITE_MAX_RETRIES &&
                now - p->last_invite_time >= INVITE_RETRY_INTERVAL) {
                Peer pcopy = *p;
                p->last_invite_time = now;
                p->invite_count++;
                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;
                }
            }
        } else if (p->remote_sink_id != 0 && p->salt_initialized) {
            Peer pcopy = *p;
            pthread_mutex_unlock(&state->peer_mutex);
            send_source_ping(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;
            }
        }
        if (p->connected && p->last_packet_time > 0 &&
            now - p->last_packet_time > PEER_TIMEOUT_SECONDS) {
            fprintf(stderr, "Peer %s timed out\n", p->name);
            p->active = false;
        }
    }
    peer_list_compact(&state->peers);

    for (int i = 0; i < state->peers.count; i++) {
        Peer *p = &state->peers.items[i];
        if (!p->active || p->is_sb_send) { continue; }
        if (p->connected && p->remote_source_id != 0 && !p->uninvited) {
            Peer pcopy = *p;
            pthread_mutex_unlock(&state->peer_mutex);
            send_uninvite_to_peer(state, &pcopy);
            pthread_mutex_lock(&state->peer_mutex);
            if (i < state->peers.count && state->peers.items[i].active) {
                state->peers.items[i].uninvited = true;
            }
        }
    }

    if (now - state->last_uninvite >= UNINVITE_ACTIVE_INTERVAL) {
        state->last_uninvite = now;
        for (int i = 0; i < state->peers.count; i++) {
            Peer *p = &state->peers.items[i];
            if (!p->active || p->is_sb_send) { continue; }
            if (p->connected && p->remote_source_id != 0) {
                Peer pcopy = *p;
                pthread_mutex_unlock(&state->peer_mutex);
                send_uninvite_to_peer(state, &pcopy);
                pthread_mutex_lock(&state->peer_mutex);
                if (i < state->peers.count && state->peers.items[i].active) {
                    state->peers.items[i].uninvited = true;
                }
            }
        }
    }

    pthread_mutex_unlock(&state->peer_mutex);
}

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

    while (atomic_load(&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, "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_message(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, "Server connection lost\n");
                atomic_store(&state->running, false);
                break;
            }
            run_maintenance(state);
        }
    }
    return NULL;
}

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

    while (atomic_load(&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) {
#ifdef _WIN32
            int err = SOCK_ERR();
#else
            int err = errno;
#endif
            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_message(state, buf, n, &from);
                packets++;
            }
        }
    }
    return NULL;
}

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    Nickname [%s]\n", DEFAULT_NICK);
    fprintf(stderr, "  -c chans   Audio channels: 1 (mono) or 2 (stereo) [%d]\n", DEFAULT_CHANNELS);
    fprintf(stderr, "  -b kbps    Opus bitrate per channel (0.5-300) [%d]\n", DEFAULT_BITRATE / 1000);
    fprintf(stderr, "  -R kbps    Requested receive bitrate from peers (0.5-300) [%d]\n", DEFAULT_BITRATE / 1000);
    fprintf(stderr, "  -F ms      Opus frame size: 2.5, 5, 10, 20, 40 [%d]\n", 20);
    fprintf(stderr, "  -C 0-10    Opus complexity [%d]\n", DEFAULT_COMPLEXITY);
    fprintf(stderr, "  -V         Use voice signal type (default: music)\n");
    fprintf(stderr, "  -J         Joint stereo (coupled streams, breaks real SonoBus)\n");
#ifdef _WIN32
    fprintf(stderr, "  -a device  Audio device name or '-' for stdin\n");
    fprintf(stderr, "  --list     List available audio capture devices\n");
#elif defined(ALSA)
    fprintf(stderr, "  -a device  ALSA device or '-' for stdin [plughw:0]\n");
    fprintf(stderr, "  --list     List available ALSA capture devices\n");
#else
    fprintf(stderr, "  -a device  (compiled without audio device support, use stdin)\n");
#endif
    fprintf(stderr, "  -h         Show help\n");
    fprintf(stderr, "  -o         Output raw audio to stdout (s16le %d Hz, all status to stderr)\n", SAMPLE_RATE);
    fprintf(stderr, "  -O         Output raw opus frames to stdout (length-prefixed)\n");
    fprintf(stderr, "  -e         Accept pre-encoded opus frames on stdin (requires -c and -b)\n");
    fprintf(stderr, "             -e -o: decode opus to raw audio on stdout\n");
    fprintf(stderr, "             -e -O: pass opus through to stdout\n");
    fprintf(stderr, "  --debug    Enable debug output\n");
    fprintf(stderr, "  --version  Show version\n");
    fprintf(stderr, "\nStdin mode: expects s16le interleaved audio at %d Hz\n", SAMPLE_RATE);
    fprintf(stderr, "Example: sox input.wav -t raw -r %d -e s -b 16 -c %d - | %s -a -\n",
           SAMPLE_RATE, DEFAULT_CHANNELS, prog);
}

void parse_args(AppState *state, int argc, char **argv) {
    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:c:b:C:R:F:a:eoOVJh")) != -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 'o': state->output_fd = STDOUT_FILENO; break;
            case 'O': state->opus_output_fd = STDOUT_FILENO; break;
            case 'e': state->use_encoded = true; break;
            case 'V': state->voice = true; break;
            case 'J': state->joint_stereo = true; break;
            case 'c':
                state->channels = atoi(optarg);
                if (state->channels != 1 && state->channels != 2) {
                    fprintf(stderr, "Error: channels must be 1 or 2\n");
                    exit(1);
                }
                break;
            case 'b':
                state->bitrate = atoi(optarg) * 1000;
                if (state->bitrate <= 0) {
                    fprintf(stderr, "Error: bitrate must be 0.5-300 kbps\n");
                    exit(1);
                }
                if (state->bitrate < 500) state->bitrate = 500;
                if (state->bitrate > 300000) state->bitrate = 300000;
                break;
            case 'R':
                state->recv_bitrate = atoi(optarg) * 1000;
                if (state->recv_bitrate <= 0) {
                    fprintf(stderr, "Error: recv bitrate must be 0.5-300 kbps\n");
                    exit(1);
                }
                if (state->recv_bitrate < 500) state->recv_bitrate = 500;
                if (state->recv_bitrate > 300000) state->recv_bitrate = 300000;
                break;
            case 'C':
                state->complexity = atoi(optarg);
                if (state->complexity < 0 || state->complexity > 10) {
                    fprintf(stderr, "Error: complexity must be 0-10\n");
                    exit(1);
                }
                break;
            case 'F': {
                double ms = atof(optarg);
                int samples = (int)(ms * SAMPLE_RATE / 1000.0);
                int min = SAMPLE_RATE / 400;
                int valid = min;
                while (valid <= samples / 2) {
                    valid *= 2;
                }
                if (valid != samples || samples < min || samples > min * 16) {
                    fprintf(stderr, "Error: frame size must be 2.5, 5, 10, 20, or 40 ms\n");
                    exit(1);
                }
                state->frame_size = samples;
                break;
            }
            case 'a':
                if (strcmp(optarg, "-") == 0) {
                    state->use_stdin = true;
                } else {
                    strncpy(state->audio_device, optarg, sizeof(state->audio_device) - 1);
                }
                break;
            case 'h':
                print_usage(argv[0]);
                exit(0);
            case ':':
                fprintf(stderr, "Error: option '-%c' requires an argument\n", optopt);
                exit(1);
            case '?':
                if (optopt != 0) {
                    fprintf(stderr, "Error: unknown option '-%c'\n", optopt);
                }
                exit(1);
            default:
                break;
        }
    }

    if (state->use_encoded) {
        if (state->channels <= 0) {
            fprintf(stderr, "Error: -e requires -c (channels)\n");
            exit(1);
        }
        if (state->bitrate <= 0) {
            fprintf(stderr, "Error: -e requires -b (bitrate)\n");
            exit(1);
        }
        state->use_stdin = true;
    }

    if (!state->use_stdin && state->audio_device[0]) {
#ifdef _WIN32
        state->use_wasapi = true;
#elif defined(ALSA)
        state->use_stdin = false;
#else
        fprintf(stderr, "Error: No audio backend compiled in. Use '-a -' for stdin.\n");
        exit(1);
#endif
    }
}

int main(int argc, char **argv) {
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "--version") == 0) {
            fprintf(stderr, "sb-send 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;
        }
        if (strcmp(argv[i], "--list") == 0) {
#ifdef _WIN32
            return wasapi_list_devices();
#elif defined(ALSA)
            return alsa_list_devices();
#else
            fprintf(stderr, "Device listing not supported (compiled without audio backend)\n");
            return 1;
#endif
        }
    }

#ifdef _WIN32
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        fprintf(stderr, "WSAStartup failed\n");
        return 1;
    }
#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.recv_bitrate = DEFAULT_BITRATE;
    state.complexity = DEFAULT_COMPLEXITY;
    state.frame_size = 960;
    state.tcp_sock = SOCK_INVALID;
    state.udp_sock = SOCK_INVALID;
    state.output_fd = -1;
    state.opus_output_fd = -1;
    state.decoder = NULL;
    state.decode_s16 = NULL;
    memcpy(state.password_hash, MD5_EMPTY, 33);

    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.last_uninvite = 0;
    state.next_sink_id = 100;
    state.client_token = ((int64_t)time(NULL) << 32) | (unsigned)getpid() | ((int64_t)1 << 63);
    srand(time(NULL) ^ (unsigned)getpid());

    parse_args(&state, argc, argv);

#ifdef _WIN32
    if (state.output_fd >= 0) {
        _setmode(_fileno(stdout), _O_BINARY);
    }
#endif

    const char *audio_source;
    if (state.use_stdin) {
        audio_source = "stdin";
    } else {
#ifdef _WIN32
        if (state.use_wasapi) {
            audio_source = state.audio_device;
        } else {
            audio_source = "stdin";
        }
#elif defined(ALSA)
        if (state.audio_device[0]) {
            audio_source = state.audio_device;
        } else {
            audio_source = "plughw:0";
        }
#else
        audio_source = "stdin";
#endif
    }

    fprintf(stderr, "sb-send %s (%d ch %d kbps/ch %dms frame %s)\n", VERSION,
           state.channels, state.bitrate / 1000,
           state.frame_size * 1000 / SAMPLE_RATE,
           audio_source);

#ifdef _WIN32
    SetConsoleCtrlHandler(console_handler, TRUE);
#else
    struct sigaction sa = { .sa_handler = signal_handler };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    signal(SIGPIPE, SIG_IGN);
#endif

    if (!state.use_stdin) {
#ifdef _WIN32
        if (state.use_wasapi) {
            if (wasapi_init_capture(&state) < 0) {
                peer_list_destroy(&state.peers);
                pthread_mutex_destroy(&state.peer_mutex);
                return 1;
            }
        }
#elif defined(ALSA)
        if (init_alsa(&state) < 0) {
            peer_list_destroy(&state.peers);
            pthread_mutex_destroy(&state.peer_mutex);
            return 1;
        }
#endif
    }

    if (!state.use_encoded && init_opus(&state) < 0) {
#ifdef _WIN32
        if (state.use_wasapi && state.wasapi_client) {
            state.wasapi_client->lpVtbl->Release(state.wasapi_client);
        }
#elif defined(ALSA)
        if (!state.use_stdin) { snd_pcm_close(state.capture); }
#endif
        peer_list_destroy(&state.peers);
        pthread_mutex_destroy(&state.peer_mutex);
        return 1;
    }

    if (state.use_encoded && state.output_fd >= 0) {
        if (init_opus_decoder(&state) < 0) {
            peer_list_destroy(&state.peers);
            pthread_mutex_destroy(&state.peer_mutex);
            return 1;
        }
        state.decode_s16 = malloc(OPUS_MAX_SAMPLES * state.channels * sizeof(int16_t));
        if (!state.decode_s16) {
            fprintf(stderr, "sb-send: decode buffer alloc failed\n");
            opus_multistream_decoder_destroy(state.decoder);
            peer_list_destroy(&state.peers);
            pthread_mutex_destroy(&state.peer_mutex);
            return 1;
        }
    }

    if (connect_and_login(&state) < 0) {
        if (state.tcp_sock != SOCK_INVALID) { CLOSE_SOCKET(state.tcp_sock); }
        if (state.udp_sock != SOCK_INVALID) { CLOSE_SOCKET(state.udp_sock); }
        if (!state.use_encoded) { opus_multistream_encoder_destroy(state.encoder); }
        if (state.decoder) { opus_multistream_decoder_destroy(state.decoder); }
        if (state.decode_s16) { free(state.decode_s16); }
#ifdef _WIN32
        if (state.use_wasapi && state.wasapi_client) {
            state.wasapi_client->lpVtbl->Release(state.wasapi_client);
        }
#elif defined(ALSA)
        if (!state.use_stdin) { snd_pcm_close(state.capture); }
#endif
        peer_list_destroy(&state.peers);
        pthread_mutex_destroy(&state.peer_mutex);
        return 1;
    }

    pthread_mutex_lock(&state.peer_mutex);
    int invite_count = 0;
    for (int i = 0; i < state.peers.count; i++) {
        if (state.peers.items[i].active && !state.peers.items[i].is_sb_send) {
            state.peers.items[i].last_invite_time = time(NULL);
            state.peers.items[i].invite_count++;
            Peer pcopy = state.peers.items[i];
            pthread_mutex_unlock(&state.peer_mutex);
            send_invite_to_peer(&state, &pcopy);
            pthread_mutex_lock(&state.peer_mutex);
            invite_count++;
        }
    }
    pthread_mutex_unlock(&state.peer_mutex);

    fprintf(stderr, "Sending invites to %d existing peers\n", invite_count);

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

    while (atomic_load(&g_running) && atomic_load(&state.running)) {
        SLEEP_MS(1000);
    }

    atomic_store(&state.running, false);
#ifdef ALSA
    if (!state.use_stdin && state.capture) {
        snd_pcm_drop(state.capture);
    }
#endif
    pthread_join(audio_tid, NULL);
    pthread_join(udp_tid, NULL);
    pthread_join(server_tid, NULL);

    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 (!state.use_encoded) { opus_multistream_encoder_destroy(state.encoder); }
    if (state.decoder) { opus_multistream_decoder_destroy(state.decoder); }
    if (state.decode_s16) { free(state.decode_s16); }
#ifdef _WIN32
    if (state.use_wasapi && state.wasapi_client) {
        state.wasapi_client->lpVtbl->Release(state.wasapi_client);
    }
    if (state.use_wasapi && state.wasapi_capture) {
        state.wasapi_capture->lpVtbl->Release(state.wasapi_capture);
    }
    WSACleanup();
#elif defined(ALSA)
    if (!state.use_stdin) { snd_pcm_drop(state.capture); snd_pcm_close(state.capture); }
#endif
    pthread_mutex_destroy(&state.peer_mutex);
    peer_list_destroy(&state.peers);
    return 0;
}
// EOF
