/*
 *  Receive TCP packets using C (multiple clients, one thread each)
 *
 *  Author: Tim Pierson, Dartmouth CS60, Fall 2026
 *          Port of Python version by Claude Opus 5
 *
 *  build: gcc -Wall -Wextra -pthread -o receive_tcp_threaded receive_tcp_threaded.c
 *  run:   ./receive_tcp_threaded
 *
 *  send data from netcat in several terminals at once (replace IP address)
 *  nc -t 127.0.0.1 9090
 *  then type messages
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define HOST "0.0.0.0"   /* "127.0.0.1" for localhost only */
#define PORT 9090
#define BUFSIZE 1024

/* what we hand to each thread. heap allocated because the accept loop's
   locals get overwritten by the next client before the thread reads them */
struct client {
    int  fd;
    char who[INET_ADDRSTRLEN + 8];   /* "a.b.c.d:port" */
};

/* send() returns how many bytes it queued, which may be fewer than asked.
   this is what Python's sendall() does for you */
static int send_all(int fd, const char *buf, size_t len)
{
    size_t sent = 0;
    while (sent < len) {
        ssize_t n = send(fd, buf + sent, len - sent, 0);
        if (n < 0) {
            if (errno == EINTR) continue;   /* interrupted, not an error */
            return -1;
        }
        sent += (size_t)n;
    }
    return 0;
}

/* everything that used to be the body of the accept loop now lives in here.
   one thread runs this per connected client, so main() is free to go
   straight back to accept() and pick up the next one */
static void *handle_client(void *arg)
{
    struct client *c = arg;
    char buf[BUFSIZE];
    char reply[BUFSIZE + 32];

    printf("Connected by %s\n", c->who);

    for (;;) {
        ssize_t n = recv(c->fd, buf, sizeof buf, 0);
        if (n == 0) {           /* peer sent FIN: orderly shutdown, not an error */
            break;
        }
        if (n < 0) {
            if (errno == EINTR) continue;
            perror("recv");
            break;
        }
        printf("Received from %s: %.*s\n", c->who, (int)n, buf);

        int len = snprintf(reply, sizeof reply, "Message received: %.*s", (int)n, buf);
        if (send_all(c->fd, reply, (size_t)len) < 0) {
            perror("send");
            break;
        }
    }

    printf("Disconnected: %s\n", c->who);
    close(c->fd);
    free(c);
    return NULL;
}

int main(void)
{
    int server_fd;
    struct sockaddr_in server_addr;

    /* create TCP socket listening on ip address and port */
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) { perror("socket"); return 1; }

    /* lets us restart right away instead of waiting out TIME_WAIT */
    int yes = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);

    memset(&server_addr, 0, sizeof server_addr);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(PORT);            /* host to network short */
    inet_pton(AF_INET, HOST, &server_addr.sin_addr);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof server_addr) < 0) {
        perror("bind"); return 1;
    }

    /* listen for connections. 128 is the backlog: how many completed
       connections may wait in the kernel before we accept() them */
    if (listen(server_fd, 128) < 0) { perror("listen"); return 1; }
    printf("Server listening on %s:%d...\n", HOST, PORT);

    for (;;) {
        struct sockaddr_in client_addr;
        socklen_t addr_len = sizeof client_addr;

        /* accept returns a new socket to the client and the client's ip/port */
        int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
        if (client_fd < 0) {
            if (errno == EINTR) continue;
            perror("accept");
            break;
        }

        struct client *c = malloc(sizeof *c);
        if (!c) { close(client_fd); continue; }
        c->fd = client_fd;

        /* inet_ntop, not inet_ntoa: ntoa returns a shared static buffer,
           so two threads formatting addresses at once corrupt each other */
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof ip);
        snprintf(c->who, sizeof c->who, "%s:%d", ip, ntohs(client_addr.sin_port));

        pthread_t tid;
        if (pthread_create(&tid, NULL, handle_client, c) != 0) {
            perror("pthread_create");
            close(client_fd);
            free(c);
            continue;
        }
        /* detach so the thread's resources are reclaimed on exit without
           anyone having to pthread_join it */
        pthread_detach(tid);
    }

    close(server_fd);
    return 0;
}