// Pide al kernel el estado de control de congestión de un socket TCP mientras envía.
//
//   ./tcp_info recv <puerto>
//   ./tcp_info send <ip> <puerto> <reno|cubic> <segundos> [ms entre muestras]
//
// El control de congestión lo hace el kernel: este programa solo lo elige con
// TCP_CONGESTION y lo observa con TCP_INFO, que es de donde saca `ss` sus números.
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

#define BUFFER_SIZE 65536
#define NO_SSTHRESH 0x7fffffffu  // el kernel usa este valor mientras no hubo ninguna pérdida

static double now_ms(void) {
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
        perror("clock_gettime");
        exit(EXIT_FAILURE);
    }
    return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
}

static const char *state_name(uint8_t state) {
    // Los nombres del kernel: enum tcp_ca_state en include/uapi/linux/tcp.h.
    switch (state) {
    case 0: return "open";
    case 1: return "disorder";
    case 2: return "cwr";
    case 3: return "recovery";
    case 4: return "loss";
    default: return "?";
    }
}

static int print_sample(int fd, double elapsed) {
    struct tcp_info info;
    socklen_t size = sizeof info;
    if (getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &size) < 0) {
        perror("getsockopt TCP_INFO");
        return -1;
    }
    char ssthresh[16] = "-";
    if (info.tcpi_snd_ssthresh < NO_SSTHRESH) {
        snprintf(ssthresh, sizeof ssthresh, "%u", info.tcpi_snd_ssthresh);
    }
    printf("%8.1f %6u %8s %8.1f %7u %s\n", elapsed, info.tcpi_snd_cwnd, ssthresh,
           info.tcpi_rtt / 1000.0, info.tcpi_total_retrans, state_name(info.tcpi_ca_state));
    return 0;
}

static int run_sender(const char *ip, int port, const char *algorithm, int seconds,
                      int interval_ms) {
    struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port)};
    if (inet_pton(AF_INET, ip, &address.sin_addr) != 1) {
        fprintf(stderr, "dirección no válida: %s\n", ip);
        return -1;
    }
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return -1;
    }
    // Se elige antes de conectar, para que también valga durante el slow start.
    if (setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, algorithm, (socklen_t)strlen(algorithm)) < 0) {
        perror("setsockopt TCP_CONGESTION");
        close(fd);
        return -1;
    }
    if (connect(fd, (struct sockaddr *)&address, sizeof address) < 0) {
        perror("connect");
        close(fd);
        return -1;
    }
    char in_use[16] = "";
    socklen_t in_use_size = sizeof in_use;
    if (getsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, in_use, &in_use_size) < 0) {
        perror("getsockopt TCP_CONGESTION");
        close(fd);
        return -1;
    }
    printf("# algoritmo: %s\n", in_use);
    printf("# %6s %6s %8s %8s %7s %s\n", "t_ms", "cwnd", "ssthresh", "rtt_ms", "retrans", "estado");

    static char buffer[BUFFER_SIZE];
    double start = now_ms();
    double next_sample = start;
    double end = start + seconds * 1000.0;
    int status = 0;
    for (;;) {
        double now = now_ms();
        if (now >= end) {
            break;
        }
        if (now >= next_sample) {
            if (print_sample(fd, now - start) < 0) {
                status = -1;
                break;
            }
            next_sample += interval_ms;
        }
        // Esperamos con poll, sin bloquear en send: la muestra debe salir a su hora
        // aunque la ventana esté llena.
        struct pollfd ready = {.fd = fd, .events = POLLOUT};
        int wait = (int)(next_sample - now) + 1;
        int events = poll(&ready, 1, wait);
        if (events < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("poll");
            status = -1;
            break;
        }
        if (events == 0) {
            continue;
        }
        if (send(fd, buffer, sizeof buffer, MSG_DONTWAIT) < 0
            && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
            perror("send");
            status = -1;
            break;
        }
    }
    if (close(fd) < 0) {
        perror("close");
        return -1;
    }
    return status;
}

static int run_receiver(int port) {
    int listener = socket(AF_INET, SOCK_STREAM, 0);
    if (listener < 0) {
        perror("socket");
        return -1;
    }
    int yes = 1;
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) < 0) {
        perror("setsockopt SO_REUSEADDR");
        close(listener);
        return -1;
    }
    struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port),
                                  .sin_addr.s_addr = htonl(INADDR_ANY)};
    if (bind(listener, (struct sockaddr *)&address, sizeof address) < 0) {
        perror("bind");
        close(listener);
        return -1;
    }
    if (listen(listener, 1) < 0) {
        perror("listen");
        close(listener);
        return -1;
    }
    int fd = accept(listener, NULL, NULL);
    if (fd < 0) {
        perror("accept");
        close(listener);
        return -1;
    }
    static char buffer[BUFFER_SIZE];
    unsigned long long total = 0;
    int status = 0;
    for (;;) {
        ssize_t got = recv(fd, buffer, sizeof buffer, 0);
        if (got < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("recv");
            status = -1;
            break;
        }
        if (got == 0) {
            break;
        }
        total += (unsigned long long)got;
    }
    printf("recibidos %llu bytes\n", total);
    if (close(fd) < 0 || close(listener) < 0) {
        perror("close");
        return -1;
    }
    return status;
}

int main(int argc, char *argv[]) {
    if (argc == 3 && strcmp(argv[1], "recv") == 0) {
        return run_receiver(atoi(argv[2])) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    if ((argc == 6 || argc == 7) && strcmp(argv[1], "send") == 0) {
        int interval_ms = argc == 7 ? atoi(argv[6]) : 100;
        if (interval_ms <= 0) {
            fprintf(stderr, "el intervalo debe ser positivo\n");
            return EXIT_FAILURE;
        }
        return run_sender(argv[2], atoi(argv[3]), argv[4], atoi(argv[5]), interval_ms) == 0
                   ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    fprintf(stderr, "uso: %s recv <puerto>\n"
                    "     %s send <ip> <puerto> <reno|cubic> <segundos> [ms entre muestras]\n",
            argv[0], argv[0]);
    return EXIT_FAILURE;
}
