// Marcador compartido: un total que varios clientes modifican a la vez, con un
// solo hilo y poll. El protocolo es de líneas de texto:
//   SUMA <n>\n    suma n al total y responde "total <t>\n"
//   TOTAL\n       responde "total <t>\n"
//   otra cosa     responde "error\n"
//
//   tally_server <puerto>
#define _POSIX_C_SOURCE 200809L

#include "io_util.h"

#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define MAX_CLIENTS 64
#define MAX_LINE 128

// Lo que el servidor recuerda de cada conexión. Con un solo hilo no hay una
// pila por cliente: la línea a medio llegar tiene que guardarse aquí.
struct client {
    char line[MAX_LINE];
    size_t used;
};

static long total = 0;

// Procesa una línea completa y deja la respuesta en reply.
static void run_command(const char *line, char *reply, size_t size) {
    long amount;
    char extra;
    if (sscanf(line, "SUMA %ld %c", &amount, &extra) == 1) {
        total += amount;
        snprintf(reply, size, "total %ld\n", total);
    } else if (strcmp(line, "TOTAL") == 0) {
        snprintf(reply, size, "total %ld\n", total);
    } else {
        snprintf(reply, size, "error\n");
    }
}

// Envía sin bloquear. Si el cliente no lee y su buffer se llenó, se le corta:
// un cliente lento no puede detener a los demás. Otra política sería guardar
// la respuesta y esperar POLLOUT. Devuelve 0, o -1 si hay que cerrar.
static int reply_to(int fd, const char *text) {
    size_t len = strlen(text);
    ssize_t sent = send(fd, text, len, MSG_DONTWAIT | MSG_NOSIGNAL);
    if (sent == -1) {
        if (errno != EAGAIN && errno != EWOULDBLOCK) {
            perror("send");
        }
        return -1;
    }
    return (size_t)sent == len ? 0 : -1;
}

// Agrega lo recibido al buffer del cliente y atiende cada línea completa.
// Devuelve 0, o -1 si hay que cerrar la conexión.
static int handle_bytes(int fd, struct client *c, const char *data, size_t n) {
    for (size_t i = 0; i < n; i++) {
        if (data[i] != '\n') {
            if (c->used == MAX_LINE - 1) {
                return -1;  // línea demasiado larga: el buffer tiene un límite
            }
            c->line[c->used++] = data[i];
            continue;
        }
        c->line[c->used] = '\0';
        c->used = 0;
        char reply[64];
        run_command(c->line, reply, sizeof reply);
        if (reply_to(fd, reply) == -1) {
            return -1;
        }
    }
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "uso: %s <puerto>\n", argv[0]);
        return EXIT_FAILURE;
    }
    int listener = tcp_listen(parse_port(argv[1]));
    if (listener == -1 || print_bound_port(listener) == -1) {
        return EXIT_FAILURE;
    }

    // fds[0] es el socket que escucha; fds[i] y clients[i] van juntos.
    struct pollfd fds[MAX_CLIENTS + 1];
    struct client clients[MAX_CLIENTS + 1];
    nfds_t count = 1;
    fds[0].fd = listener;
    fds[0].events = POLLIN;

    for (;;) {
        // Dormir hasta que algún socket tenga algo. Es el único punto de espera.
        if (poll(fds, count, -1) == -1) {
            if (errno == EINTR) {
                continue;
            }
            perror("poll");
            return EXIT_FAILURE;
        }

        // Recorremos de atrás hacia adelante: quitar un cliente mueve al último
        // a su lugar, y ese ya fue revisado.
        for (nfds_t i = count; i-- > 1;) {
            if ((fds[i].revents & (POLLIN | POLLERR | POLLHUP)) == 0) {
                continue;
            }
            char buf[512];
            ssize_t n = recv(fds[i].fd, buf, sizeof buf, 0);
            if (n == -1 && errno == EINTR) {
                continue;
            }
            if (n == -1) {
                perror("recv");
            }
            if (n <= 0 || handle_bytes(fds[i].fd, &clients[i], buf, (size_t)n) == -1) {
                close_checked(fds[i].fd);
                count--;
                fds[i] = fds[count];
                clients[i] = clients[count];
            }
        }

        // poll dijo que hay una conexión esperando en la cola de accept.
        if (fds[0].revents & POLLIN) {
            int conn = accept(listener, NULL, NULL);
            if (conn == -1) {
                perror("accept");
            } else if (count == MAX_CLIENTS + 1) {
                fprintf(stderr, "demasiados clientes\n");
                close_checked(conn);
            } else {
                fds[count].fd = conn;
                fds[count].events = POLLIN;
                fds[count].revents = 0;
                clients[count].used = 0;
                count++;
            }
        }
    }
}
