#define _POSIX_C_SOURCE 200809L
#include <sys/socket.h>
#include <netdb.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int
write_all(int fd, const char *buf, size_t len)
{
    while (len > 0) {
        ssize_t n = write(fd, buf, len);
        if (n < 0 && errno == EINTR) continue;
        if (n <= 0) return -1;
        buf += n;
        len -= (size_t)n;
    }
    return 0;
}

/* Result 2 allows a final stdin line without a newline; not a wire message. */
static int
read_line(int fd, char *line, size_t size)
{
    size_t len = 0;
    for (;;) {
        char c;
        ssize_t n = read(fd, &c, 1);
        if (n < 0 && errno == EINTR) continue;
        if (n < 0) return -1;
        if (n == 0 || c == '\n') {
            line[len] = '\0';
            if (n == 0) return len == 0 ? 0 : 2;
            return 1;
        }
        if (c == '\0') return -3;
        if (len + 1 >= size) return -2;
        line[len++] = c;
    }
}

static int
connect_server(const char *host, const char *port)
{
    struct addrinfo hints = {0}, *addresses, *addr;
    int fd = -1, result, saved = ECONNREFUSED;
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_NUMERICSERV;
    result = getaddrinfo(host, port, &hints, &addresses);
    if (result != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result));
        return -1;
    }
    for (addr = addresses; addr != NULL; addr = addr->ai_next) {
        do {
            fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
            if (fd < 0) {
                saved = errno;
                break;
            }
            result = connect(fd, addr->ai_addr, addr->ai_addrlen);
            if (result == 0) break;
            saved = errno;
            close(fd);
            fd = -1;
        } while (saved == EINTR);
        if (fd >= 0) break;
    }
    freeaddrinfo(addresses);
    if (fd < 0) {
        errno = saved;
        perror("connect");
    }
    return fd;
}

int
main(int argc, char **argv)
{
    const char *host = argc > 1 ? argv[1] : "127.0.0.1";
    const char *port = argc > 2 ? argv[2] : "9000";
    char line[4097], reply[4097], *end;
    struct sigaction action = {0};
    long number;
    int fd, status = 0;

    if (argc > 3) {
        fprintf(stderr, "usage: %s [host] [port]\n", argv[0]);
        return 1;
    }
    errno = 0;
    number = strtol(port, &end, 10);
    if (errno || !*port || *end || number < 1 || number > 65535) {
        fprintf(stderr, "invalid port: %s\n", port);
        return 1;
    }
    action.sa_handler = SIG_IGN;
    sigemptyset(&action.sa_mask);
    if (sigaction(SIGPIPE, &action, NULL) < 0) {
        perror("sigaction");
        return 1;
    }
    fd = connect_server(host, port);
    if (fd < 0) return 1;
    for (;;) {
        int result = read_line(STDIN_FILENO, line, sizeof(line));
        if (result == 0) break;
        if (result < 0) {
            if (result == -1) perror("stdin");
            else fprintf(stderr, "%s\n", result == -2 ?
                         "input line too long" : "input contains a NUL byte");
            status = 1;
            break;
        }
        if (write_all(fd, line, strlen(line)) < 0 ||
            write_all(fd, "\n", 1) < 0) {
            perror("write");
            status = 1;
            break;
        }
        result = read_line(fd, reply, sizeof(reply));
        if (result == 0) break;
        if (result != 1) {
            if (result == -1) perror("read");
            else fprintf(stderr, "invalid or incomplete server response\n");
            status = 1;
            break;
        }
        if (write_all(STDOUT_FILENO, reply, strlen(reply)) < 0 ||
            write_all(STDOUT_FILENO, "\n", 1) < 0) {
            perror("stdout");
            status = 1;
            break;
        }
        if (strcmp(line, "QUIT") == 0) break;
    }
    close(fd);
    return status;
}
